diff --git a/ambient.d.ts b/ambient.d.ts index 80812920..1c5527d1 100644 --- a/ambient.d.ts +++ b/ambient.d.ts @@ -32,3 +32,80 @@ declare module '@mojaloop/central-services-metrics' declare module '@hapi/good' declare module 'hapi-openapi' declare module 'blipp' + +// version 2.4 -> https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/javascript-state-machine/index.d.ts +// we are using ^3.1.0 +declare module 'javascript-state-machine' { + type Method = (...args: unknown[]) => void | Promise + type Data = Record + + + interface Transition { + name: string; + from: string; + to: string; + } + interface TransitionEvent { + transition: string; + from: string; + to: string; + fsm: JSM; + event: string; + } + interface StateMachineConfig { + init?: string; + transitions: Transition[]; + data?: Data; + methods?: Record; + } + + interface StateMachineInterface { + // current state + state: string; + + // return true if state s is the current state + is(s: string): boolean + + // return true if transition t can occur from the current state + can(t: string): boolean + + // return true if transition t cannot occur from the current state + cannot(t: string): boolean + + // return list of transitions that are allowed from the current state + transitions(): string[] + + // return list of all possible transitions + allTransitions(): string[] + + // return list of all possible states + allStates(): string [] + } + export default class StateMachine { + constructor(config: StateMachineConfig) + + // current state + state: string; + + // return true if state s is the current state + is(s: string): boolean + + // return true if transition t can occur from the current state + can(t: string): boolean + + // return true if transition t cannot occur from the current state + cannot(t: string): boolean + + // return list of transitions that are allowed from the current state + transitions(): string[] + + // return list of all possible transitions + allTransitions(): string[] + + // return list of all possible states + allStates(): string [] + + static factory(spec: StateMachineConfig): StateMachine + } + +} diff --git a/config/development.json b/config/development.json index 451fddcc..6e803964 100644 --- a/config/development.json +++ b/config/development.json @@ -2,6 +2,11 @@ "PORT": 4004, "HOST": "0.0.0.0", "PARTICIPANT_ID": "auth_service", + "REDIS": { + "PORT": 6379, + "HOST": "redis", + "TIMEOUT": 100 + }, "INSPECT": { "DEPTH": 4, "SHOW_HIDDEN": false, @@ -66,4 +71,4 @@ ] } } -} \ No newline at end of file +} diff --git a/config/integration.json b/config/integration.json index 451fddcc..6e803964 100644 --- a/config/integration.json +++ b/config/integration.json @@ -2,6 +2,11 @@ "PORT": 4004, "HOST": "0.0.0.0", "PARTICIPANT_ID": "auth_service", + "REDIS": { + "PORT": 6379, + "HOST": "redis", + "TIMEOUT": 100 + }, "INSPECT": { "DEPTH": 4, "SHOW_HIDDEN": false, @@ -66,4 +71,4 @@ ] } } -} \ No newline at end of file +} diff --git a/config/production.json b/config/production.json index 451fddcc..6e803964 100644 --- a/config/production.json +++ b/config/production.json @@ -2,6 +2,11 @@ "PORT": 4004, "HOST": "0.0.0.0", "PARTICIPANT_ID": "auth_service", + "REDIS": { + "PORT": 6379, + "HOST": "redis", + "TIMEOUT": 100 + }, "INSPECT": { "DEPTH": 4, "SHOW_HIDDEN": false, @@ -66,4 +71,4 @@ ] } } -} \ No newline at end of file +} diff --git a/config/test.json b/config/test.json index 024938c0..299dca10 100644 --- a/config/test.json +++ b/config/test.json @@ -2,6 +2,11 @@ "PORT": 4004, "HOST": "0.0.0.0", "PARTICIPANT_ID": "auth_service", + "REDIS": { + "PORT": 6379, + "HOST": "localhost", + "TIMEOUT": 100 + }, "INSPECT": { "DEPTH": 4, "SHOW_HIDDEN": false, @@ -49,4 +54,4 @@ ] } } -} \ No newline at end of file +} diff --git a/docker-compose.linux.yml b/docker-compose.linux.yml new file mode 100644 index 00000000..f6aab29e --- /dev/null +++ b/docker-compose.linux.yml @@ -0,0 +1,20 @@ +# This is an extension docker-compose file that contains `extra_hosts` entries to work arouind networking +# on linux, start docker-local with the following command +# +# docker-compose -f docker-compose.yml -f docker-compose.linux.yml up -d +# +# I suspect that with some of the recent changes to the way containers refer to one another inside the +# docker-local environment, this will no longer be needed. Please delete this file if you are reading this message +# past July and we haven't run into issues... + +version: "3.7" + +services: + auth-service: + extra_hosts: + - "redis:172.17.0.1" + - "ml-testing-toolkit:172.17.0.1" + + ml-testing-toolkit: + extra_hosts: + - "auth-service:172.17.0.1" diff --git a/docker-compose.yml b/docker-compose.yml index b8d936ec..b15d1cd5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,11 +18,12 @@ services: - mojaloop-net depends_on: - mysql + - redis volumes: - ./scripts/wait4.js:/opt/auth-service/scripts/wait4.js - ./scripts/wait4.config.js:/opt/auth-service/scripts/wait4.config.js healthcheck: - test: wget -q http://172.17.0.1:4004/health -O /dev/null || exit 1 + test: wget -q http://localhost:4004/health -O /dev/null || exit 1 timeout: 20s retries: 30 interval: 15s @@ -46,4 +47,46 @@ services: timeout: 20s retries: 10 start_period: 40s - interval: 30s \ No newline at end of file + interval: 30s + + redis: + container_name: redis + image: "redis:6.2.4-alpine" + networks: + - mojaloop-net + ports: + - "6379:6379" + restart: always + + ml-testing-toolkit: + image: mojaloop/ml-testing-toolkit:v12.4.0 + container_name: ml-testing-toolkit + volumes: + - "./docker/ml-testing-toolkit/spec_files:/opt/mojaloop-testing-toolkit/spec_files" + - "./docker/ml-testing-toolkit/secrets:/opt/mojaloop-testing-toolkit/secrets" + ports: + - "15000:5000" + - "5050:5050" + command: npm start + networks: + - mojaloop-net + depends_on: + - mongo + + ml-testing-toolkit-ui: + image: mojaloop/ml-testing-toolkit-ui:v12.0.0 + container_name: ml-testing-toolkit-ui + ports: + - "6060:6060" + command: nginx -g "daemon off;" + depends_on: + - ml-testing-toolkit + - mongo + networks: + - mojaloop-net + + mongo: + image: mongo + container_name: 3p_mongo + ports: + - "27017:27017" diff --git a/docker/ml-testing-toolkit/secrets/privatekey.pem b/docker/ml-testing-toolkit/secrets/privatekey.pem new file mode 100644 index 00000000..e69de29b diff --git a/docker/ml-testing-toolkit/secrets/publickey.cer b/docker/ml-testing-toolkit/secrets/publickey.cer new file mode 100644 index 00000000..23bdf7ab --- /dev/null +++ b/docker/ml-testing-toolkit/secrets/publickey.cer @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbjCCAlYCCQDudXfDH36/JjANBgkqhkiG9w0BAQsFADB5MRswGQYDVQQDDBJ0 +ZXN0aW5ndG9vbGtpdGRmc3AxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARPaGlvMREw +DwYDVQQHDAhDb2x1bWJ1czEYMBYGA1UECgwPVGVzdGluZyBUb29sa2l0MREwDwYD +VQQLDAhQYXltZW50czAeFw0yMDAzMjQxNzU1MjZaFw0yNTAzMjMxNzU1MjZaMHkx +GzAZBgNVBAMMEnRlc3Rpbmd0b29sa2l0ZGZzcDELMAkGA1UEBhMCVVMxDTALBgNV +BAgMBE9oaW8xETAPBgNVBAcMCENvbHVtYnVzMRgwFgYDVQQKDA9UZXN0aW5nIFRv +b2xraXQxETAPBgNVBAsMCFBheW1lbnRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAwczEjlUeOPutgPRlpZSbcbJJwsmmxsBfoPDw1sjBiR7L6DohVqKd +810+TmiDRYgCzOLabje/mtLiDC95MtuPF5yUiVE04ar6Ny5pZLxJEnbDEOAETxOn +1gzCKeRHYOcgybDi6TLhnvyFyIaXKzyBhEYvxI8VvRV11UawLqvpgVrdsbZy1FQO +MLq7OB+J6qC7fhR61F6Wu45RZlZMB482c658P7dCQCdQtEMEF5kuBNB/JuURe0qK +jl2udKVL3wgBC7J7o7Tx8kY5T63q/ZC3TfoTclFeXtIePt8Eu74u3d6WpSWbZ12m +ewRBVPtmbGHgEXpih3uayaqIeC8Dc4zO5QIDAQABMA0GCSqGSIb3DQEBCwUAA4IB +AQAZ1lQ/KcSGwy/jQUIGF87JugLU17nnIEG2TrkC5n+fZDQqs8QqU6itbkdGQyNj +F5aLoPEdrKzevnBztlAEq0bofR0uDnQPN74A/NwOUfWds0hq5elZnO9Uq0G15Go4 +pfqLbSjHxSu6LZaHP6f9+WvMqNbGr3kipz8GSIQWixzdKBnNxCwWjZmk4gD5cahU +XIpMAZumsnKk6pWilmuMIxC579CyLkGdVze3Kj6GunUJ1pieZzv4+RUJz8NgXxjW +ZRwqCkEqPe/8S1X9srtcrdbHryDdC18Ldu/rADEKbSqy0BhQdKYDcxulaQuqibwD +i0dWSdTWoseAbUqp2ACc6aF/ +-----END CERTIFICATE----- diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/api_spec.yaml b/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/api_spec.yaml new file mode 100644 index 00000000..dabade89 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/api_spec.yaml @@ -0,0 +1,3719 @@ +openapi: 3.0.2 +info: + version: '1.1' + title: Open API for FSP Interoperability (FSPIOP) + description: >- + Based on [API Definition updated on 2020-05-19 Version + 1.1](https://github.com/mojaloop/mojaloop-specification/blob/master/documents/v1.1-document-set/API%20Definition_v1.1.pdf). + + + **Note:** The API supports a maximum size of 65536 bytes (64 Kilobytes) in + the HTTP header. + license: + name: CC BY-ND 4.0 + url: 'https://github.com/mojaloop/mojaloop-specification/blob/master/LICENSE.md' + contact: + name: Sam Kummary + url: 'https://github.com/mojaloop/mojaloop-specification/issues' +servers: + - url: 'protocol://hostname:/switch/' + variables: + protocol: + enum: + - http + - https + default: https +paths: + /interface: + post: + description: >- + Essential path to include schema definitions that are not used so that + these definitions get included into the openapi-cli bundle api + definition so that they get converted into typescript definitions. + operationId: test + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/BinaryString' + - $ref: '#/components/schemas/BinaryString32' + - $ref: '#/components/schemas/Date' + - $ref: '#/components/schemas/Integer' + - $ref: '#/components/schemas/Name' + - $ref: '#/components/schemas/PersonalIdentifierType' + - $ref: '#/components/schemas/TokenCode' + - $ref: '#/components/schemas/Transaction' + - $ref: '#/components/schemas/UndefinedEnum' + responses: + '200': + description: Ok + '/participants/{Type}/{ID}': + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + post: + description: >- + The HTTP request `POST /participants/{Type}/{ID}` (or `POST + /participants/{Type}/{ID}/{SubId}`) is used to create information in the + server regarding the provided identity, defined by `{Type}`, `{ID}`, and + optionally `{SubId}` (for example, `POST /participants/MSISDN/123456789` + or `POST /participants/BUSINESS/shoecompany/employee1`). An + ExtensionList element has been added to this reqeust in version v1.1 + summary: Create participant information + tags: + - participants + operationId: ParticipantsByIDAndType + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Participant information to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantsTypeIDSubIDPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + get: + description: >- + The HTTP request `GET /participants/{Type}/{ID}` (or `GET + /participants/{Type}/{ID}/{SubId}`) is used to find out in which FSP the + requested Party, defined by `{Type}`, `{ID}` and optionally `{SubId}`, + is located (for example, `GET /participants/MSISDN/123456789`, or `GET + /participants/BUSINESS/shoecompany/employee1`). This HTTP request should + support a query string for filtering of currency. To use filtering of + currency, the HTTP request `GET /participants/{Type}/{ID}?currency=XYZ` + should be used, where `XYZ` is the requested currency. + summary: Look up participant information + tags: + - participants + operationId: ParticipantsByTypeAndID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /participants/{Type}/{ID}` (or `PUT + /participants/{Type}/{ID}/{SubId}`) is used to inform the client of a + successful result of the lookup, creation, or deletion of the FSP + information related to the Party. If the FSP information is deleted, the + fspId element should be empty; otherwise the element should include the + FSP information for the Party. + summary: Return participant information + tags: + - participants + operationId: ParticipantsByTypeAndID3 + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Participant information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantsTypeIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + delete: + description: >- + The HTTP request `DELETE /participants/{Type}/{ID}` (or `DELETE + /participants/{Type}/{ID}/{SubId}`) is used to delete information in the + server regarding the provided identity, defined by `{Type}` and `{ID}`) + (for example, `DELETE /participants/MSISDN/123456789`), and optionally + `{SubId}`. This HTTP request should support a query string to delete FSP + information regarding a specific currency only. To delete a specific + currency only, the HTTP request `DELETE + /participants/{Type}/{ID}?currency=XYZ` should be used, where `XYZ` is + the requested currency. + + + **Note:** The Account Lookup System should verify that it is the Party’s + current FSP that is deleting the FSP information. + summary: Delete participant information + tags: + - participants + operationId: ParticipantsByTypeAndID2 + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/participants/{Type}/{ID}/error': + put: + description: >- + If the server is unable to find, create or delete the associated FSP of + the provided identity, or another processing error occurred, the error + callback `PUT /participants/{Type}/{ID}/error` (or `PUT + /participants/{Type}/{ID}/{SubId}/error`) is used. + summary: Return participant information error + tags: + - participants + operationId: ParticipantsErrorByTypeAndID + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/participants/{Type}/{ID}/{SubId}': + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/SubId' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + post: + description: >- + The HTTP request `POST /participants/{Type}/{ID}` (or `POST + /participants/{Type}/{ID}/{SubId}`) is used to create information in the + server regarding the provided identity, defined by `{Type}`, `{ID}`, and + optionally `{SubId}` (for example, `POST /participants/MSISDN/123456789` + or `POST /participants/BUSINESS/shoecompany/employee1`). An + ExtensionList element has been added to this reqeust in version v1.1 + summary: Create participant information + tags: + - participants + operationId: ParticipantsSubIdByTypeAndIDPost + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Participant information to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantsTypeIDSubIDPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + get: + description: >- + The HTTP request `GET /participants/{Type}/{ID}` (or `GET + /participants/{Type}/{ID}/{SubId}`) is used to find out in which FSP the + requested Party, defined by `{Type}`, `{ID}` and optionally `{SubId}`, + is located (for example, `GET /participants/MSISDN/123456789`, or `GET + /participants/BUSINESS/shoecompany/employee1`). This HTTP request should + support a query string for filtering of currency. To use filtering of + currency, the HTTP request `GET /participants/{Type}/{ID}?currency=XYZ` + should be used, where `XYZ` is the requested currency. + summary: Look up participant information + tags: + - participants + operationId: ParticipantsSubIdByTypeAndID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /participants/{Type}/{ID}` (or `PUT + /participants/{Type}/{ID}/{SubId}`) is used to inform the client of a + successful result of the lookup, creation, or deletion of the FSP + information related to the Party. If the FSP information is deleted, the + fspId element should be empty; otherwise the element should include the + FSP information for the Party. + summary: Return participant information + tags: + - participants + operationId: ParticipantsSubIdByTypeAndID3 + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Participant information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantsTypeIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + delete: + description: >- + The HTTP request `DELETE /participants/{Type}/{ID}` (or `DELETE + /participants/{Type}/{ID}/{SubId}`) is used to delete information in the + server regarding the provided identity, defined by `{Type}` and `{ID}`) + (for example, `DELETE /participants/MSISDN/123456789`), and optionally + `{SubId}`. This HTTP request should support a query string to delete FSP + information regarding a specific currency only. To delete a specific + currency only, the HTTP request `DELETE + /participants/{Type}/{ID}?currency=XYZ` should be used, where `XYZ` is + the requested currency. + + + **Note:** The Account Lookup System should verify that it is the Party’s + current FSP that is deleting the FSP information. + summary: Delete participant information + tags: + - participants + operationId: ParticipantsSubIdByTypeAndID2 + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/participants/{Type}/{ID}/{SubId}/error': + put: + description: >- + If the server is unable to find, create or delete the associated FSP of + the provided identity, or another processing error occurred, the error + callback `PUT /participants/{Type}/{ID}/error` (or `PUT + /participants/{Type}/{ID}/{SubId}/error`) is used. + summary: Return participant information error + tags: + - participants + operationId: ParticipantsSubIdErrorByTypeAndID + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/SubId' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /participants: + post: + description: >- + The HTTP request `POST /participants` is used to create information in + the server regarding the provided list of identities. This request + should be used for bulk creation of FSP information for more than one + Party. The optional currency parameter should indicate that each + provided Party supports the currency. + summary: Create bulk participant information + tags: + - participants + operationId: Participants1 + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Participant information to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantsPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/participants/{ID}': + put: + description: >- + The callback `PUT /participants/{ID}` is used to inform the client of + the result of the creation of the provided list of identities. + summary: Return bulk participant information + tags: + - participants + operationId: putParticipantsByID + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Participant information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantsIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/participants/{ID}/error': + put: + description: >- + If there is an error during FSP information creation in the server, the + error callback `PUT /participants/{ID}/error` is used. The `{ID}` in the + URI should contain the requestId that was used for the creation of the + participant information. + summary: Return bulk participant information error + tags: + - participants + operationId: ParticipantsByIDAndError + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/parties/{Type}/{ID}': + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: >- + The HTTP request `GET /parties/{Type}/{ID}` (or `GET + /parties/{Type}/{ID}/{SubId}`) is used to look up information regarding + the requested Party, defined by `{Type}`, `{ID}` and optionally + `{SubId}` (for example, `GET /parties/MSISDN/123456789`, or `GET + /parties/BUSINESS/shoecompany/employee1`). + summary: Look up party information + tags: + - parties + operationId: PartiesByTypeAndID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /parties/{Type}/{ID}` (or `PUT + /parties/{Type}/{ID}/{SubId}`) is used to inform the client of a + successful result of the Party information lookup. + summary: Return party information + tags: + - parties + operationId: PartiesByTypeAndID2 + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Party information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PartiesTypeIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/parties/{Type}/{ID}/error': + put: + description: >- + If the server is unable to find Party information of the provided + identity, or another processing error occurred, the error callback `PUT + /parties/{Type}/{ID}/error` (or `PUT /parties/{Type}/{ID}/{SubI}/error`) + is used. + summary: Return party information error + tags: + - parties + operationId: PartiesErrorByTypeAndID + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/parties/{Type}/{ID}/{SubId}': + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/SubId' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: >- + The HTTP request `GET /parties/{Type}/{ID}` (or `GET + /parties/{Type}/{ID}/{SubId}`) is used to look up information regarding + the requested Party, defined by `{Type}`, `{ID}` and optionally + `{SubId}` (for example, `GET /parties/MSISDN/123456789`, or `GET + /parties/BUSINESS/shoecompany/employee1`). + summary: Look up party information + tags: + - parties + operationId: PartiesSubIdByTypeAndID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /parties/{Type}/{ID}` (or `PUT + /parties/{Type}/{ID}/{SubId}`) is used to inform the client of a + successful result of the Party information lookup. + summary: Return party information + tags: + - parties + operationId: PartiesSubIdByTypeAndIDPut + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Party information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PartiesTypeIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/parties/{Type}/{ID}/{SubId}/error': + put: + description: >- + If the server is unable to find Party information of the provided + identity, or another processing error occurred, the error callback `PUT + /parties/{Type}/{ID}/error` (or `PUT + /parties/{Type}/{ID}/{SubId}/error`) is used. + summary: Return party information error + tags: + - parties + operationId: PartiesSubIdErrorByTypeAndID + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/SubId' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /transactionRequests: + post: + description: >- + The HTTP request `POST /transactionRequests` is used to request the + creation of a transaction request for the provided financial transaction + in the server. + summary: Perform transaction request + tags: + - transactionRequests + operationId: TransactionRequests + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Transaction request to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionRequestsPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/transactionRequests/{ID}': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: >- + The HTTP request `GET /transactionRequests/{ID}` is used to get + information regarding a transaction request created or requested + earlier. The `{ID}` in the URI should contain the `transactionRequestId` + that was used for the creation of the transaction request. + summary: Retrieve transaction request information + tags: + - transactionRequests + operationId: TransactionRequestsByID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /transactionRequests/{ID}` is used to inform the + client of a requested or created transaction request. The `{ID}` in the + URI should contain the `transactionRequestId` that was used for the + creation of the transaction request, or the `{ID}` that was used in the + `GET /transactionRequests/{ID}`. + summary: Return transaction request information + tags: + - transactionRequests + operationId: TransactionRequestsByIDPut + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Transaction request information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionRequestsIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/transactionRequests/{ID}/error': + put: + description: >- + If the server is unable to find or create a transaction request, or + another processing error occurs, the error callback `PUT + /transactionRequests/{ID}/error` is used. The `{ID}` in the URI should + contain the `transactionRequestId` that was used for the creation of the + transaction request, or the `{ID}` that was used in the `GET + /transactionRequests/{ID}`. + summary: Return transaction request information error + tags: + - transactionRequests + operationId: TransactionRequestsErrorByID + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /quotes: + post: + description: >- + The HTTP request `POST /quotes` is used to request the creation of a + quote for the provided financial transaction in the server. + summary: Calculate quote + tags: + - quotes + operationId: Quotes + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the quote to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/QuotesPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/quotes/{ID}': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: >- + The HTTP request `GET /quotes/{ID}` is used to get information regarding + a quote created or requested earlier. The `{ID}` in the URI should + contain the `quoteId` that was used for the creation of the quote. + summary: Retrieve quote information + tags: + - quotes + operationId: QuotesByID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /quotes/{ID}` is used to inform the client of a + requested or created quote. The `{ID}` in the URI should contain the + `quoteId` that was used for the creation of the quote, or the `{ID}` + that was used in the `GET /quotes/{ID}` request. + summary: Return quote information + tags: + - quotes + operationId: QuotesByID1 + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Quote information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/QuotesIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/quotes/{ID}/error': + put: + description: >- + If the server is unable to find or create a quote, or some other + processing error occurs, the error callback `PUT /quotes/{ID}/error` is + used. The `{ID}` in the URI should contain the `quoteId` that was used + for the creation of the quote, or the `{ID}` that was used in the `GET + /quotes/{ID}` request. + summary: Return quote information error + tags: + - quotes + operationId: QuotesByIDAndError + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /transfers: + post: + description: >- + The HTTP request `POST /transfers` is used to request the creation of a + transfer for the next ledger, and a financial transaction for the Payee + FSP. + summary: Perform transfer + tags: + - transfers + operationId: transfers + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the transfer to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransfersPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/transfers/{ID}': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: >- + The HTTP request `GET /transfers/{ID}` is used to get information + regarding a transfer created or requested earlier. The `{ID}` in the URI + should contain the `transferId` that was used for the creation of the + transfer. + summary: Retrieve transfer information + tags: + - transfers + operationId: TransfersByIDGet + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + patch: + description: >- + The HTTP request PATCH /transfers/ is used by a Switch to update the + state of a previously reserved transfer, if the Payee FSP has requested + a commit notification when the Switch has completed processing of the + transfer. The in the URI should contain the transferId that was + used for the creation of the transfer. Please note that this request + does not generate a callback. + summary: Return transfer information + tags: + - transfers + operationId: TransfersByIDPatch + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Transfer notification upon completion. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransfersIDPatchResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /transfers/{ID}` is used to inform the client of a + requested or created transfer. The `{ID}` in the URI should contain the + `transferId` that was used for the creation of the transfer, or the + `{ID}` that was used in the `GET /transfers/{ID}` request. + summary: Return transfer information + tags: + - transfers + operationId: TransfersByIDPut + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Transfer information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransfersIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/transfers/{ID}/error': + put: + description: >- + If the server is unable to find or create a transfer, or another + processing error occurs, the error callback `PUT /transfers/{ID}/error` + is used. The `{ID}` in the URI should contain the `transferId` that was + used for the creation of the transfer, or the `{ID}` that was used in + the `GET /transfers/{ID}`. + summary: Return transfer information error + tags: + - transfers + operationId: TransfersByIDAndError + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/transactions/{ID}': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: >- + The HTTP request `GET /transactions/{ID}` is used to get transaction + information regarding a financial transaction created earlier. The + `{ID}` in the URI should contain the `transactionId` that was used for + the creation of the quote, as the transaction is created as part of + another process (the transfer process). + summary: Retrieve transaction information + tags: + - transactions + operationId: TransactionsByID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /transactions/{ID}` is used to inform the client of a + requested transaction. The `{ID}` in the URI should contain the `{ID}` + that was used in the `GET /transactions/{ID}` request. + summary: Return transaction information + tags: + - transactions + operationId: TransactionsByID1 + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Transaction information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionsIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/transactions/{ID}/error': + put: + description: >- + If the server is unable to find or create a transaction, or another + processing error occurs, the error callback `PUT + /transactions/{ID}/error` is used. The `{ID}` in the URI should contain + the `{ID}` that was used in the `GET /transactions/{ID}` request. + summary: Return transaction information error + tags: + - transactions + operationId: TransactionsErrorByID + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /bulkQuotes: + post: + description: >- + The HTTP request `POST /bulkQuotes` is used to request the creation of a + bulk quote for the provided financial transactions in the server. + summary: Calculate bulk quote + tags: + - bulkQuotes + operationId: BulkQuotes + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the bulk quote to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BulkQuotesPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/bulkQuotes/{ID}': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: >- + The HTTP request `GET /bulkQuotes/{ID}` is used to get information + regarding a bulk quote created or requested earlier. The `{ID}` in the + URI should contain the `bulkQuoteId` that was used for the creation of + the bulk quote. + summary: Retrieve bulk quote information + tags: + - bulkQuotes + operationId: BulkQuotesByID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /bulkQuotes/{ID}` is used to inform the client of a + requested or created bulk quote. The `{ID}` in the URI should contain + the `bulkQuoteId` that was used for the creation of the bulk quote, or + the `{ID}` that was used in the `GET /bulkQuotes/{ID}` request. + summary: Return bulk quote information + tags: + - bulkQuotes + operationId: BulkQuotesByID1 + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Bulk quote information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BulkQuotesIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/bulkQuotes/{ID}/error': + put: + description: >- + If the server is unable to find or create a bulk quote, or another + processing error occurs, the error callback `PUT /bulkQuotes/{ID}/error` + is used. The `{ID}` in the URI should contain the `bulkQuoteId` that was + used for the creation of the bulk quote, or the `{ID}` that was used in + the `GET /bulkQuotes/{ID}` request. + summary: Return bulk quote information error + tags: + - bulkQuotes + operationId: BulkQuotesErrorByID + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + /bulkTransfers: + post: + description: >- + The HTTP request `POST /bulkTransfers` is used to request the creation + of a bulk transfer in the server. + summary: Perform bulk transfer + tags: + - bulkTransfers + operationId: BulkTransfers + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the bulk transfer to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BulkTransfersPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/bulkTransfers/{ID}': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: >- + The HTTP request `GET /bulkTransfers/{ID}` is used to get information + regarding a bulk transfer created or requested earlier. The `{ID}` in + the URI should contain the `bulkTransferId` that was used for the + creation of the bulk transfer. + summary: Retrieve bulk transfer information + tags: + - bulkTransfers + operationId: BulkTransferByID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /bulkTransfers/{ID}` is used to inform the client of a + requested or created bulk transfer. The `{ID}` in the URI should contain + the `bulkTransferId` that was used for the creation of the bulk transfer + (`POST /bulkTransfers`), or the `{ID}` that was used in the `GET + /bulkTransfers/{ID}` request. + summary: Return bulk transfer information + tags: + - bulkTransfers + operationId: BulkTransfersByIDPut + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Bulk transfer information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BulkTransfersIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/bulkTransfers/{ID}/error': + put: + description: >- + If the server is unable to find or create a bulk transfer, or another + processing error occurs, the error callback `PUT + /bulkTransfers/{ID}/error` is used. The `{ID}` in the URI should contain + the `bulkTransferId` that was used for the creation of the bulk transfer + (`POST /bulkTransfers`), or the `{ID}` that was used in the `GET + /bulkTransfers/{ID}` request. + summary: Return bulk transfer information error + tags: + - bulkTransfers + operationId: BulkTransfersErrorByID + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Length' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + requestBody: + description: Details of the error returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' +components: + schemas: + BinaryString: + type: string + pattern: '^[A-Za-z0-9-_]+[=]{0,2}$' + description: >- + The API data type BinaryString is a JSON String. The string is a + base64url encoding of a string of raw bytes, where padding (character + ‘=’) is added at the end of the data if needed to ensure that the string + is a multiple of 4 characters. The length restriction indicates the + allowed number of characters. + BinaryString32: + type: string + pattern: '^[A-Za-z0-9-_]{43}$' + description: >- + The API data type BinaryString32 is a fixed size version of the API data + type BinaryString, where the raw underlying data is always of 32 bytes. + The data type BinaryString32 should not use a padding character as the + size of the underlying data is fixed. + Date: + title: Date + type: string + pattern: >- + ^(?:[1-9]\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)$ + description: >- + The API data type Date is a JSON String in a lexical format that is + restricted by a regular expression for interoperability reasons. This + format, as specified in ISO 8601, contains a date only. A more readable + version of the format is yyyy-MM-dd. Examples are "1982-05-23", + "1987-08-05”. + Integer: + title: Integer + type: string + pattern: '^[1-9]\d*$' + description: >- + The API data type Integer is a JSON String consisting of digits only. + Negative numbers and leading zeroes are not allowed. The data type is + always limited to a specific number of digits. + Name: + title: Name + type: string + pattern: '^(?!\s*$)[\w .,''-]{1,128}$' + description: >- + The API data type Name is a JSON String, restricted by a regular + expression to avoid characters which are generally not used in a name. + + + Regular Expression - The regular expression for restricting the Name + type is "^(?!\s*$)[\w .,'-]{1,128}$". The restriction does not allow a + string consisting of whitespace only, all Unicode characters are + allowed, as well as the period (.) (apostrophe (‘), dash (-), comma (,) + and space characters ( ). + + + **Note:** In some programming languages, Unicode support must be + specifically enabled. For example, if Java is used, the flag + UNICODE_CHARACTER_CLASS must be enabled to allow Unicode characters. + PersonalIdentifierType: + title: PersonalIdentifierType + type: string + enum: + - PASSPORT + - NATIONAL_REGISTRATION + - DRIVING_LICENSE + - ALIEN_REGISTRATION + - NATIONAL_ID_CARD + - EMPLOYER_ID + - TAX_ID_NUMBER + - SENIOR_CITIZENS_CARD + - MARRIAGE_CERTIFICATE + - HEALTH_CARD + - VOTERS_ID + - UNITED_NATIONS + - OTHER_ID + description: >- + Below are the allowed values for the enumeration. + + - PASSPORT - A passport number is used as reference to a Party. + + - NATIONAL_REGISTRATION - A national registration number is used as + reference to a Party. + + - DRIVING_LICENSE - A driving license is used as reference to a Party. + + - ALIEN_REGISTRATION - An alien registration number is used as reference + to a Party. + + - NATIONAL_ID_CARD - A national ID card number is used as reference to a + Party. + + - EMPLOYER_ID - A tax identification number is used as reference to a + Party. + + - TAX_ID_NUMBER - A tax identification number is used as reference to a + Party. + + - SENIOR_CITIZENS_CARD - A senior citizens card number is used as + reference to a Party. + + - MARRIAGE_CERTIFICATE - A marriage certificate number is used as + reference to a Party. + + - HEALTH_CARD - A health card number is used as reference to a Party. + + - VOTERS_ID - A voter’s identification number is used as reference to a + Party. + + - UNITED_NATIONS - An UN (United Nations) number is used as reference to + a Party. + + - OTHER_ID - Any other type of identification type number is used as + reference to a Party. + TokenCode: + title: TokenCode + type: string + pattern: '^[0-9a-zA-Z]{4,32}$' + description: >- + The API data type TokenCode is a JSON String between 4 and 32 + characters, consisting of digits or upper- or lowercase characters from + a to z. + CorrelationId: + title: CorrelationId + type: string + pattern: >- + ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ + description: >- + Identifier that correlates all messages of the same sequence. The API + data type UUID (Universally Unique Identifier) is a JSON String in + canonical format, conforming to [RFC + 4122](https://tools.ietf.org/html/rfc4122), that is restricted by a + regular expression for interoperability reasons. A UUID is always 36 + characters long, 32 hexadecimal symbols and 4 dashes (‘-‘). + example: b51ec534-ee48-4575-b6a9-ead2955b8069 + PartyIdType: + title: PartyIdType + type: string + enum: + - MSISDN + - EMAIL + - PERSONAL_ID + - BUSINESS + - DEVICE + - ACCOUNT_ID + - IBAN + - ALIAS + - THIRD_PARTY_LINK # there is a need to update quotes endpoint def to accept this value + description: >- + Below are the allowed values for the enumeration. + + - MSISDN - An MSISDN (Mobile Station International Subscriber Directory + Number, that is, the phone number) is used as reference to a + participant. The MSISDN identifier should be in international format + according to the [ITU-T E.164 + standard](https://www.itu.int/rec/T-REC-E.164/en). Optionally, the + MSISDN may be prefixed by a single plus sign, indicating the + international prefix. + + - EMAIL - An email is used as reference to a participant. The format of + the email should be according to the informational [RFC + 3696](https://tools.ietf.org/html/rfc3696). + + - PERSONAL_ID - A personal identifier is used as reference to a + participant. Examples of personal identification are passport number, + birth certificate number, and national registration number. The + identifier number is added in the PartyIdentifier element. The personal + identifier type is added in the PartySubIdOrType element. + + - BUSINESS - A specific Business (for example, an organization or a + company) is used as reference to a participant. The BUSINESS identifier + can be in any format. To make a transaction connected to a specific + username or bill number in a Business, the PartySubIdOrType element + should be used. + + - DEVICE - A specific device (for example, a POS or ATM) ID connected to + a specific business or organization is used as reference to a Party. For + referencing a specific device under a specific business or organization, + use the PartySubIdOrType element. + + - ACCOUNT_ID - A bank account number or FSP account ID should be used as + reference to a participant. The ACCOUNT_ID identifier can be in any + format, as formats can greatly differ depending on country and FSP. + + - IBAN - A bank account number or FSP account ID is used as reference to + a participant. The IBAN identifier can consist of up to 34 alphanumeric + characters and should be entered without whitespace. + + - ALIAS An alias is used as reference to a participant. The alias should + be created in the FSP as an alternative reference to an account owner. + Another example of an alias is a username in the FSP system. The ALIAS + identifier can be in any format. It is also possible to use the + PartySubIdOrType element for identifying an account under an Alias + defined by the PartyIdentifier. + PartyIdentifier: + title: PartyIdentifier + type: string + minLength: 1 + maxLength: 128 + description: Identifier of the Party. + example: '16135551212' + PartySubIdOrType: + title: PartySubIdOrType + type: string + minLength: 1 + maxLength: 128 + description: >- + Either a sub-identifier of a PartyIdentifier, or a sub-type of the + PartyIdType, normally a PersonalIdentifierType. + FspId: + title: FspId + type: string + minLength: 1 + maxLength: 32 + description: FSP identifier. + ExtensionKey: + title: ExtensionKey + type: string + minLength: 1 + maxLength: 32 + description: Extension key. + ExtensionValue: + title: ExtensionValue + type: string + minLength: 1 + maxLength: 128 + description: Extension value. + Extension: + title: Extension + type: object + description: Data model for the complex type Extension. + properties: + key: + $ref: '#/components/schemas/ExtensionKey' + value: + $ref: '#/components/schemas/ExtensionValue' + required: + - key + - value + ExtensionList: + title: ExtensionList + type: object + description: >- + Data model for the complex type ExtensionList. An optional list of + extensions, specific to deployment. + properties: + extension: + type: array + items: + $ref: '#/components/schemas/Extension' + minItems: 1 + maxItems: 16 + description: Number of Extension elements. + required: + - extension + PartyIdInfo: + title: PartyIdInfo + type: object + description: >- + Data model for the complex type PartyIdInfo. An ExtensionList element + has been added to this reqeust in version v1.1 + properties: + partyIdType: + $ref: '#/components/schemas/PartyIdType' + partyIdentifier: + $ref: '#/components/schemas/PartyIdentifier' + partySubIdOrType: + $ref: '#/components/schemas/PartySubIdOrType' + fspId: + $ref: '#/components/schemas/FspId' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - partyIdType + - partyIdentifier + MerchantClassificationCode: + title: MerchantClassificationCode + type: string + pattern: '^[\d]{1,4}$' + description: >- + A limited set of pre-defined numbers. This list would be a limited set + of numbers identifying a set of popular merchant types like School Fees, + Pubs and Restaurants, Groceries, etc. + PartyName: + title: PartyName + type: string + minLength: 1 + maxLength: 128 + description: Name of the Party. Could be a real name or a nickname. + FirstName: + title: FirstName + type: string + minLength: 1 + maxLength: 128 + pattern: '^(?!\s*$)[\w .,''-]{1,128}$' + description: First name of the Party (Name Type). + example: Henrik + MiddleName: + title: MiddleName + type: string + minLength: 1 + maxLength: 128 + pattern: '^(?!\s*$)[\w .,''-]{1,128}$' + description: Middle name of the Party (Name Type). + example: Johannes + LastName: + title: LastName + type: string + minLength: 1 + maxLength: 128 + pattern: '^(?!\s*$)[\w .,''-]{1,128}$' + description: Last name of the Party (Name Type). + example: Karlsson + PartyComplexName: + title: PartyComplexName + type: object + description: Data model for the complex type PartyComplexName. + properties: + firstName: + $ref: '#/components/schemas/FirstName' + middleName: + $ref: '#/components/schemas/MiddleName' + lastName: + $ref: '#/components/schemas/LastName' + DateOfBirth: + title: DateofBirth (type Date) + type: string + pattern: >- + ^(?:[1-9]\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)$ + description: Date of Birth of the Party. + example: '1966-06-16' + PartyPersonalInfo: + title: PartyPersonalInfo + type: object + description: Data model for the complex type PartyPersonalInfo. + properties: + complexName: + $ref: '#/components/schemas/PartyComplexName' + dateOfBirth: + $ref: '#/components/schemas/DateOfBirth' + Party: + title: Party + type: object + description: Data model for the complex type Party. + properties: + partyIdInfo: + $ref: '#/components/schemas/PartyIdInfo' + merchantClassificationCode: + $ref: '#/components/schemas/MerchantClassificationCode' + name: + $ref: '#/components/schemas/PartyName' + personalInfo: + $ref: '#/components/schemas/PartyPersonalInfo' + required: + - partyIdInfo + Currency: + title: Currency + description: >- + The currency codes defined in [ISO + 4217](https://www.iso.org/iso-4217-currency-codes.html) as three-letter + alphabetic codes are used as the standard naming representation for + currencies. + type: string + minLength: 3 + maxLength: 3 + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KMF + - KPW + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRO + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLL + - SOS + - SPL + - SRD + - STD + - SVC + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VEF + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWD + Amount: + title: Amount + type: string + pattern: '^([0]|([1-9][0-9]{0,17}))([.][0-9]{0,3}[1-9])?$' + description: >- + The API data type Amount is a JSON String in a canonical format that is + restricted by a regular expression for interoperability reasons. This + pattern does not allow any trailing zeroes at all, but allows an amount + without a minor currency unit. It also only allows four digits in the + minor currency unit; a negative value is not allowed. Using more than 18 + digits in the major currency unit is not allowed. + example: '123.45' + Money: + title: Money + type: object + description: Data model for the complex type Money. + properties: + currency: + $ref: '#/components/schemas/Currency' + amount: + $ref: '#/components/schemas/Amount' + required: + - currency + - amount + TransactionScenario: + title: TransactionScenario + type: string + enum: + - DEPOSIT + - WITHDRAWAL + - TRANSFER + - PAYMENT + - REFUND + description: >- + Below are the allowed values for the enumeration. + + - DEPOSIT - Used for performing a Cash-In (deposit) transaction. In a + normal scenario, electronic funds are transferred from a Business + account to a Consumer account, and physical cash is given from the + Consumer to the Business User. + + - WITHDRAWAL - Used for performing a Cash-Out (withdrawal) transaction. + In a normal scenario, electronic funds are transferred from a Consumer’s + account to a Business account, and physical cash is given from the + Business User to the Consumer. + + - TRANSFER - Used for performing a P2P (Peer to Peer, or Consumer to + Consumer) transaction. + + - PAYMENT - Usually used for performing a transaction from a Consumer to + a Merchant or Organization, but could also be for a B2B (Business to + Business) payment. The transaction could be online for a purchase in an + Internet store, in a physical store where both the Consumer and Business + User are present, a bill payment, a donation, and so on. + + - REFUND - Used for performing a refund of transaction. + example: DEPOSIT + TransactionSubScenario: + title: TransactionSubScenario + type: string + pattern: '^[A-Z_]{1,32}$' + description: >- + Possible sub-scenario, defined locally within the scheme (UndefinedEnum + Type). + example: LOCALLY_DEFINED_SUBSCENARIO + TransactionInitiator: + title: TransactionInitiator + type: string + enum: + - PAYER + - PAYEE + description: >- + Below are the allowed values for the enumeration. + + - PAYER - Sender of funds is initiating the transaction. The account to + send from is either owned by the Payer or is connected to the Payer in + some way. + + - PAYEE - Recipient of the funds is initiating the transaction by + sending a transaction request. The Payer must approve the transaction, + either automatically by a pre-generated OTP or by pre-approval of the + Payee, or by manually approving in his or her own Device. + example: PAYEE + TransactionInitiatorType: + title: TransactionInitiatorType + type: string + enum: + - CONSUMER + - AGENT + - BUSINESS + - DEVICE + description: |- + Below are the allowed values for the enumeration. + - CONSUMER - Consumer is the initiator of the transaction. + - AGENT - Agent is the initiator of the transaction. + - BUSINESS - Business is the initiator of the transaction. + - DEVICE - Device is the initiator of the transaction. + example: CONSUMER + RefundReason: + title: RefundReason + type: string + minLength: 1 + maxLength: 128 + description: Reason for the refund. + example: Free text indicating reason for the refund. + Refund: + title: Refund + type: object + description: Data model for the complex type Refund. + properties: + originalTransactionId: + $ref: '#/components/schemas/CorrelationId' + refundReason: + $ref: '#/components/schemas/RefundReason' + required: + - originalTransactionId + BalanceOfPayments: + title: BalanceOfPayments + type: string + pattern: '^[1-9]\d{2}$' + description: >- + (BopCode) The API data type + [BopCode](https://www.imf.org/external/np/sta/bopcode/) is a JSON String + of 3 characters, consisting of digits only. Negative numbers are not + allowed. A leading zero is not allowed. + example: '123' + TransactionType: + title: TransactionType + type: object + description: Data model for the complex type TransactionType. + properties: + scenario: + $ref: '#/components/schemas/TransactionScenario' + subScenario: + $ref: '#/components/schemas/TransactionSubScenario' + initiator: + $ref: '#/components/schemas/TransactionInitiator' + initiatorType: + $ref: '#/components/schemas/TransactionInitiatorType' + refundInfo: + $ref: '#/components/schemas/Refund' + balanceOfPayments: + $ref: '#/components/schemas/BalanceOfPayments' + required: + - scenario + - initiator + - initiatorType + Note: + title: Note + type: string + minLength: 1 + maxLength: 128 + description: Memo assigned to transaction. + example: Note sent to Payee. + Transaction: + title: Transaction + type: object + description: >- + Data model for the complex type Transaction. The Transaction type is + used to carry end-to-end data between the Payer FSP and the Payee FSP in + the ILP Packet. Both the transactionId and the quoteId in the data model + are decided by the Payer FSP in the POST /quotes request. + properties: + transactionId: + $ref: '#/components/schemas/CorrelationId' + quoteId: + $ref: '#/components/schemas/CorrelationId' + payee: + $ref: '#/components/schemas/Party' + payer: + $ref: '#/components/schemas/Party' + amount: + $ref: '#/components/schemas/Money' + transactionType: + $ref: '#/components/schemas/TransactionType' + note: + $ref: '#/components/schemas/Note' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transactionId + - quoteId + - payee + - payer + - amount + - transactionType + UndefinedEnum: + title: UndefinedEnum + type: string + pattern: '^[A-Z_]{1,32}$' + description: >- + The API data type UndefinedEnum is a JSON String consisting of 1 to 32 + uppercase characters including an underscore character (_). + ErrorCode: + title: ErrorCode + type: string + pattern: '^[1-9]\d{3}$' + description: >- + The API data type ErrorCode is a JSON String of four characters, + consisting of digits only. Negative numbers are not allowed. A leading + zero is not allowed. Each error code in the API is a four-digit number, + for example, 1234, where the first number (1 in the example) represents + the high-level error category, the second number (2 in the example) + represents the low-level error category, and the last two numbers (34 in + the example) represent the specific error. + example: '5100' + ErrorDescription: + title: ErrorDescription + type: string + minLength: 1 + maxLength: 128 + description: Error description string. + ErrorInformation: + title: ErrorInformation + type: object + description: Data model for the complex type ErrorInformation. + properties: + errorCode: + $ref: '#/components/schemas/ErrorCode' + errorDescription: + $ref: '#/components/schemas/ErrorDescription' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - errorCode + - errorDescription + ErrorInformationResponse: + title: ErrorInformationResponse + type: object + description: >- + Data model for the complex type object that contains an optional element + ErrorInformation used along with 4xx and 5xx responses. + properties: + errorInformation: + $ref: '#/components/schemas/ErrorInformation' + ParticipantsTypeIDPutResponse: + title: ParticipantsTypeIDPutResponse + type: object + description: >- + The object sent in the PUT /participants/{Type}/{ID}/{SubId} and + /participants/{Type}/{ID} callbacks. + properties: + fspId: + $ref: '#/components/schemas/FspId' + ParticipantsTypeIDSubIDPostRequest: + title: ParticipantsTypeIDSubIDPostRequest + type: object + description: >- + The object sent in the POST /participants/{Type}/{ID}/{SubId} and + /participants/{Type}/{ID} requests. An additional optional ExtensionList + element has been added as part of v1.1 changes. + properties: + fspId: + $ref: '#/components/schemas/FspId' + currency: + $ref: '#/components/schemas/Currency' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - fspId + ErrorInformationObject: + title: ErrorInformationObject + type: object + description: Data model for the complex type object that contains ErrorInformation. + properties: + errorInformation: + $ref: '#/components/schemas/ErrorInformation' + required: + - errorInformation + ParticipantsPostRequest: + title: ParticipantsPostRequest + type: object + description: The object sent in the POST /participants request. + properties: + requestId: + $ref: '#/components/schemas/CorrelationId' + partyList: + type: array + items: + $ref: '#/components/schemas/PartyIdInfo' + minItems: 1 + maxItems: 10000 + description: >- + List of PartyIdInfo elements that the client would like to update or + create FSP information about. + currency: + $ref: '#/components/schemas/Currency' + required: + - requestId + - partyList + PartyResult: + title: PartyResult + type: object + description: Data model for the complex type PartyResult. + properties: + partyId: + $ref: '#/components/schemas/PartyIdInfo' + errorInformation: + $ref: '#/components/schemas/ErrorInformation' + required: + - partyId + ParticipantsIDPutResponse: + title: ParticipantsIDPutResponse + type: object + description: 'The object sent in the PUT /participants/{ID} callback.' + properties: + partyList: + type: array + items: + $ref: '#/components/schemas/PartyResult' + minItems: 1 + maxItems: 10000 + description: >- + List of PartyResult elements that were either created or failed to + be created. + currency: + $ref: '#/components/schemas/Currency' + required: + - partyList + PartiesTypeIDPutResponse: + title: PartiesTypeIDPutResponse + type: object + description: 'The object sent in the PUT /parties/{Type}/{ID} callback.' + properties: + party: + $ref: '#/components/schemas/Party' + required: + - party + Latitude: + title: Latitude + type: string + pattern: >- + ^(\+|-)?(?:90(?:(?:\.0{1,6})?)|(?:[0-9]|[1-8][0-9])(?:(?:\.[0-9]{1,6})?))$ + description: >- + The API data type Latitude is a JSON String in a lexical format that is + restricted by a regular expression for interoperability reasons. + example: '+45.4215' + Longitude: + title: Longitude + type: string + pattern: >- + ^(\+|-)?(?:180(?:(?:\.0{1,6})?)|(?:[0-9]|[1-9][0-9]|1[0-7][0-9])(?:(?:\.[0-9]{1,6})?))$ + description: >- + The API data type Longitude is a JSON String in a lexical format that is + restricted by a regular expression for interoperability reasons. + example: '+75.6972' + GeoCode: + title: GeoCode + type: object + description: >- + Data model for the complex type GeoCode. Indicates the geographic + location from where the transaction was initiated. + properties: + latitude: + $ref: '#/components/schemas/Latitude' + longitude: + $ref: '#/components/schemas/Longitude' + required: + - latitude + - longitude + AuthenticationType: + title: AuthenticationType + type: string + enum: + - OTP + - QRCODE + - U2F + description: |- + Below are the allowed values for the enumeration AuthenticationType. + - OTP - One-time password generated by the Payer FSP. + - QRCODE - QR code used as One Time Password. + - U2F - U2F is a new addition isolated to Thirdparty stream. + example: OTP + DateTime: + title: DateTime + type: string + pattern: >- + ^(?:[1-9]\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:(\.\d{3}))(?:Z|[+-][01]\d:[0-5]\d)$ + description: >- + The API data type DateTime is a JSON String in a lexical format that is + restricted by a regular expression for interoperability reasons. The + format is according to [ISO + 8601](https://www.iso.org/iso-8601-date-and-time-format.html), expressed + in a combined date, time and time zone format. A more readable version + of the format is yyyy-MM-ddTHH:mm:ss.SSS[-HH:MM]. Examples are + "2016-05-24T08:38:08.699-04:00", "2016-05-24T08:38:08.699Z" (where Z + indicates Zulu time zone, same as UTC). + example: '2016-05-24T08:38:08.699-04:00' + TransactionRequestsPostRequest: + title: TransactionRequestsPostRequest + type: object + description: The object sent in the POST /transactionRequests request. + properties: + transactionRequestId: + $ref: '#/components/schemas/CorrelationId' + payee: + $ref: '#/components/schemas/Party' + payer: + $ref: '#/components/schemas/PartyIdInfo' + amount: + $ref: '#/components/schemas/Money' + transactionType: + $ref: '#/components/schemas/TransactionType' + note: + $ref: '#/components/schemas/Note' + geoCode: + $ref: '#/components/schemas/GeoCode' + authenticationType: + $ref: '#/components/schemas/AuthenticationType' + expiration: + $ref: '#/components/schemas/DateTime' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transactionRequestId + - payee + - payer + - amount + - transactionType + TransactionRequestState: + title: TransactionRequestState + type: string + enum: + - RECEIVED + - PENDING + - ACCEPTED + - REJECTED + description: |- + Below are the allowed values for the enumeration. + - RECEIVED - Payer FSP has received the transaction from the Payee FSP. + - PENDING - Payer FSP has sent the transaction request to the Payer. + - ACCEPTED - Payer has approved the transaction. + - REJECTED - Payer has rejected the transaction. + example: RECEIVED + TransactionRequestsIDPutResponse: + title: TransactionRequestsIDPutResponse + type: object + description: 'The object sent in the PUT /transactionRequests/{ID} callback.' + properties: + transactionId: + $ref: '#/components/schemas/CorrelationId' + transactionRequestState: + $ref: '#/components/schemas/TransactionRequestState' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transactionRequestState + AmountType: + title: AmountType + type: string + enum: + - SEND + - RECEIVE + description: >- + Below are the allowed values for the enumeration AmountType. + + - SEND - Amount the Payer would like to send, that is, the amount that + should be withdrawn from the Payer account including any fees. + + - RECEIVE - Amount the Payer would like the Payee to receive, that is, + the amount that should be sent to the receiver exclusive of any fees. + example: RECEIVE + QuotesPostRequest: + title: QuotesPostRequest + type: object + description: The object sent in the POST /quotes request. + properties: + quoteId: + $ref: '#/components/schemas/CorrelationId' + transactionId: + $ref: '#/components/schemas/CorrelationId' + transactionRequestId: + $ref: '#/components/schemas/CorrelationId' + payee: + $ref: '#/components/schemas/Party' + payer: + $ref: '#/components/schemas/Party' + amountType: + $ref: '#/components/schemas/AmountType' + amount: + $ref: '#/components/schemas/Money' + fees: + $ref: '#/components/schemas/Money' + transactionType: + $ref: '#/components/schemas/TransactionType' + geoCode: + $ref: '#/components/schemas/GeoCode' + note: + $ref: '#/components/schemas/Note' + expiration: + $ref: '#/components/schemas/DateTime' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - quoteId + - transactionId + - payee + - payer + - amountType + - amount + - transactionType + IlpPacket: + title: IlpPacket + type: string + pattern: '^[A-Za-z0-9-_]+[=]{0,2}$' + minLength: 1 + maxLength: 32768 + description: Information for recipient (transport layer information). + example: >- + AYIBgQAAAAAAAASwNGxldmVsb25lLmRmc3AxLm1lci45T2RTOF81MDdqUUZERmZlakgyOVc4bXFmNEpLMHlGTFGCAUBQU0svMS4wCk5vbmNlOiB1SXlweUYzY3pYSXBFdzVVc05TYWh3CkVuY3J5cHRpb246IG5vbmUKUGF5bWVudC1JZDogMTMyMzZhM2ItOGZhOC00MTYzLTg0NDctNGMzZWQzZGE5OGE3CgpDb250ZW50LUxlbmd0aDogMTM1CkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbgpTZW5kZXItSWRlbnRpZmllcjogOTI4MDYzOTEKCiJ7XCJmZWVcIjowLFwidHJhbnNmZXJDb2RlXCI6XCJpbnZvaWNlXCIsXCJkZWJpdE5hbWVcIjpcImFsaWNlIGNvb3BlclwiLFwiY3JlZGl0TmFtZVwiOlwibWVyIGNoYW50XCIsXCJkZWJpdElkZW50aWZpZXJcIjpcIjkyODA2MzkxXCJ9IgA + IlpCondition: + title: IlpCondition + type: string + pattern: '^[A-Za-z0-9-_]{43}$' + maxLength: 48 + description: Condition that must be attached to the transfer by the Payer. + QuotesIDPutResponse: + title: QuotesIDPutResponse + type: object + description: 'The object sent in the PUT /quotes/{ID} callback.' + properties: + transferAmount: + $ref: '#/components/schemas/Money' + payeeReceiveAmount: + $ref: '#/components/schemas/Money' + payeeFspFee: + $ref: '#/components/schemas/Money' + payeeFspCommission: + $ref: '#/components/schemas/Money' + expiration: + $ref: '#/components/schemas/DateTime' + geoCode: + $ref: '#/components/schemas/GeoCode' + ilpPacket: + $ref: '#/components/schemas/IlpPacket' + condition: + $ref: '#/components/schemas/IlpCondition' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transferAmount + - expiration + - ilpPacket + - condition + OtpValue: + title: OtpValue + type: string + pattern: '^\d{3,10}$' + description: >- + The API data type OtpValue is a JSON String of 3 to 10 characters, + consisting of digits only. Negative numbers are not allowed. One or more + leading zeros are allowed. + QRCODE: + title: QRCODE + type: string + minLength: 1 + maxLength: 64 + description: QR code used as a One Time Password. + U2FPIN: + title: U2FPIN + type: string + pattern: '^\S{1,64}$' + minLength: 1 + maxLength: 64 + description: > + U2F challenge-response, where payer FSP verifies if the response + provided by end-user device matches the previously registered key. + U2FPinValue: + title: U2FPinValue + type: object + description: > + U2F challenge-response, where payer FSP verifies if the response + provided by end-user device matches the previously registered key. + properties: + pinValue: + allOf: + - $ref: '#/components/schemas/U2FPIN' + description: U2F challenge-response. + counter: + allOf: + - $ref: '#/components/schemas/Integer' + description: >- + Sequential counter used for cloning detection. Present only for U2F + authentication. + required: + - pinValue + - counter + AuthenticationValue: + title: AuthenticationValue + anyOf: + - $ref: '#/components/schemas/OtpValue' + - $ref: '#/components/schemas/QRCODE' + - $ref: '#/components/schemas/U2FPinValue' + pattern: '^\d{3,10}$|^\S{1,64}$' + description: >- + Contains the authentication value. The format depends on the + authentication type used in the AuthenticationInfo complex type. + AuthenticationInfo: + title: AuthenticationInfo + type: object + description: Data model for the complex type AuthenticationInfo. + properties: + authentication: + $ref: '#/components/schemas/AuthenticationType' + authenticationValue: + $ref: '#/components/schemas/AuthenticationValue' + required: + - authentication + - authenticationValue + AuthorizationResponse: + title: AuthorizationResponse + type: string + enum: + - ENTERED + - REJECTED + - RESEND + description: |- + Below are the allowed values for the enumeration. + - ENTERED - Consumer entered the authentication value. + - REJECTED - Consumer rejected the transaction. + - RESEND - Consumer requested to resend the authentication value. + example: ENTERED + AuthorizationsIDPutResponse: + title: AuthorizationsIDPutResponse + type: object + description: 'The object sent in the PUT /authorizations/{ID} callback.' + properties: + authenticationInfo: + $ref: '#/components/schemas/AuthenticationInfo' + responseType: + $ref: '#/components/schemas/AuthorizationResponse' + required: + - responseType + TransfersPostRequest: + title: TransfersPostRequest + type: object + description: The object sent in the POST /transfers request. + properties: + transferId: + $ref: '#/components/schemas/CorrelationId' + payeeFsp: + $ref: '#/components/schemas/FspId' + payerFsp: + $ref: '#/components/schemas/FspId' + amount: + $ref: '#/components/schemas/Money' + ilpPacket: + $ref: '#/components/schemas/IlpPacket' + condition: + $ref: '#/components/schemas/IlpCondition' + expiration: + $ref: '#/components/schemas/DateTime' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transferId + - payeeFsp + - payerFsp + - amount + - ilpPacket + - condition + - expiration + IlpFulfilment: + title: IlpFulfilment + type: string + pattern: '^[A-Za-z0-9-_]{43}$' + maxLength: 48 + description: Fulfilment that must be attached to the transfer by the Payee. + example: WLctttbu2HvTsa1XWvUoGRcQozHsqeu9Ahl2JW9Bsu8 + TransferState: + title: TransferState + type: string + enum: + - RECEIVED + - RESERVED + - COMMITTED + - ABORTED + description: >- + Below are the allowed values for the enumeration. + + - RECEIVED - Next ledger has received the transfer. + + - RESERVED - Next ledger has reserved the transfer. + + - COMMITTED - Next ledger has successfully performed the transfer. + + - ABORTED - Next ledger has aborted the transfer due to a rejection or + failure to perform the transfer. + example: RESERVED + TransfersIDPutResponse: + title: TransfersIDPutResponse + type: object + description: 'The object sent in the PUT /transfers/{ID} callback.' + properties: + fulfilment: + $ref: '#/components/schemas/IlpFulfilment' + completedTimestamp: + $ref: '#/components/schemas/DateTime' + transferState: + $ref: '#/components/schemas/TransferState' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transferState + TransfersIDPatchResponse: + title: TransfersIDPatchResponse + type: object + description: 'PATCH /transfers/{ID} object' + properties: + completedTimestamp: + $ref: '#/components/schemas/DateTime' + transferState: + $ref: '#/components/schemas/TransferState' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - completedTimestamp + - transferState + TransactionState: + title: TransactionState + type: string + enum: + - RECEIVED + - PENDING + - COMPLETED + - REJECTED + description: |- + Below are the allowed values for the enumeration. + - RECEIVED - Payee FSP has received the transaction from the Payer FSP. + - PENDING - Payee FSP has validated the transaction. + - COMPLETED - Payee FSP has successfully performed the transaction. + - REJECTED - Payee FSP has failed to perform the transaction. + example: RECEIVED + Code: + title: Code + type: string + pattern: '^[0-9a-zA-Z]{4,32}$' + description: Any code/token returned by the Payee FSP (TokenCode Type). + example: Test-Code + TransactionsIDPutResponse: + title: TransactionsIDPutResponse + type: object + description: 'The object sent in the PUT /transactions/{ID} callback.' + properties: + completedTimestamp: + $ref: '#/components/schemas/DateTime' + transactionState: + $ref: '#/components/schemas/TransactionState' + code: + $ref: '#/components/schemas/Code' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transactionState + IndividualQuote: + title: IndividualQuote + type: object + description: Data model for the complex type IndividualQuote. + properties: + quoteId: + $ref: '#/components/schemas/CorrelationId' + transactionId: + $ref: '#/components/schemas/CorrelationId' + payee: + $ref: '#/components/schemas/Party' + amountType: + $ref: '#/components/schemas/AmountType' + amount: + $ref: '#/components/schemas/Money' + fees: + $ref: '#/components/schemas/Money' + transactionType: + $ref: '#/components/schemas/TransactionType' + note: + $ref: '#/components/schemas/Note' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - quoteId + - transactionId + - payee + - amountType + - amount + - transactionType + BulkQuotesPostRequest: + title: BulkQuotesPostRequest + type: object + description: The object sent in the POST /bulkQuotes request. + properties: + bulkQuoteId: + $ref: '#/components/schemas/CorrelationId' + payer: + $ref: '#/components/schemas/Party' + geoCode: + $ref: '#/components/schemas/GeoCode' + expiration: + $ref: '#/components/schemas/DateTime' + individualQuotes: + type: array + minItems: 1 + maxItems: 1000 + items: + $ref: '#/components/schemas/IndividualQuote' + description: List of quotes elements. + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - bulkQuoteId + - payer + - individualQuotes + IndividualQuoteResult: + title: IndividualQuoteResult + type: object + description: Data model for the complex type IndividualQuoteResult. + properties: + quoteId: + $ref: '#/components/schemas/CorrelationId' + payee: + $ref: '#/components/schemas/Party' + transferAmount: + $ref: '#/components/schemas/Money' + payeeReceiveAmount: + $ref: '#/components/schemas/Money' + payeeFspFee: + $ref: '#/components/schemas/Money' + payeeFspCommission: + $ref: '#/components/schemas/Money' + ilpPacket: + $ref: '#/components/schemas/IlpPacket' + condition: + $ref: '#/components/schemas/IlpCondition' + errorInformation: + $ref: '#/components/schemas/ErrorInformation' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - quoteId + BulkQuotesIDPutResponse: + title: BulkQuotesIDPutResponse + type: object + description: 'The object sent in the PUT /bulkQuotes/{ID} callback.' + properties: + individualQuoteResults: + type: array + maxItems: 1000 + items: + $ref: '#/components/schemas/IndividualQuoteResult' + description: >- + Fees for each individual transaction, if any of them are charged per + transaction. + expiration: + $ref: '#/components/schemas/DateTime' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - expiration + IndividualTransfer: + title: IndividualTransfer + type: object + description: Data model for the complex type IndividualTransfer. + properties: + transferId: + $ref: '#/components/schemas/CorrelationId' + transferAmount: + $ref: '#/components/schemas/Money' + ilpPacket: + $ref: '#/components/schemas/IlpPacket' + condition: + $ref: '#/components/schemas/IlpCondition' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transferId + - transferAmount + - ilpPacket + - condition + BulkTransfersPostRequest: + title: BulkTransfersPostRequest + type: object + description: The object sent in the POST /bulkTransfers request. + properties: + bulkTransferId: + $ref: '#/components/schemas/CorrelationId' + bulkQuoteId: + $ref: '#/components/schemas/CorrelationId' + payerFsp: + $ref: '#/components/schemas/FspId' + payeeFsp: + $ref: '#/components/schemas/FspId' + individualTransfers: + type: array + minItems: 1 + maxItems: 1000 + items: + $ref: '#/components/schemas/IndividualTransfer' + description: List of IndividualTransfer elements. + expiration: + $ref: '#/components/schemas/DateTime' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - bulkTransferId + - bulkQuoteId + - payerFsp + - payeeFsp + - individualTransfers + - expiration + IndividualTransferResult: + title: IndividualTransferResult + type: object + description: Data model for the complex type IndividualTransferResult. + properties: + transferId: + $ref: '#/components/schemas/CorrelationId' + fulfilment: + $ref: '#/components/schemas/IlpFulfilment' + errorInformation: + $ref: '#/components/schemas/ErrorInformation' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - transferId + BulkTransferState: + title: BulkTransactionState + type: string + enum: + - RECEIVED + - PENDING + - ACCEPTED + - PROCESSING + - COMPLETED + - REJECTED + description: >- + Below are the allowed values for the enumeration. + + - RECEIVED - Payee FSP has received the bulk transfer from the Payer + FSP. + + - PENDING - Payee FSP has validated the bulk transfer. + + - ACCEPTED - Payee FSP has accepted to process the bulk transfer. + + - PROCESSING - Payee FSP has started to transfer fund to the Payees. + + - COMPLETED - Payee FSP has completed transfer of funds to the Payees. + + - REJECTED - Payee FSP has rejected to process the bulk transfer. + example: RECEIVED + BulkTransfersIDPutResponse: + title: BulkTransfersIDPutResponse + type: object + description: 'The object sent in the PUT /bulkTransfers/{ID} callback.' + properties: + completedTimestamp: + $ref: '#/components/schemas/DateTime' + individualTransferResults: + type: array + maxItems: 1000 + items: + $ref: '#/components/schemas/IndividualTransferResult' + description: List of IndividualTransferResult elements. + bulkTransferState: + $ref: '#/components/schemas/BulkTransferState' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - bulkTransferState + parameters: + Type: + name: Type + in: path + required: true + schema: + type: string + description: 'The type of the party identifier. For example, `MSISDN`, `PERSONAL_ID`.' + ID: + name: ID + in: path + required: true + schema: + type: string + description: The identifier value. + Content-Type: + name: Content-Type + in: header + schema: + type: string + required: true + description: >- + The `Content-Type` header indicates the specific version of the API used + to send the payload body. + Date: + name: Date + in: header + schema: + type: string + required: true + description: The `Date` header field indicates the date when the request was sent. + X-Forwarded-For: + name: X-Forwarded-For + in: header + schema: + type: string + required: false + description: >- + The `X-Forwarded-For` header field is an unofficially accepted standard + used for informational purposes of the originating client IP address, as + a request might pass multiple proxies, firewalls, and so on. Multiple + `X-Forwarded-For` values should be expected and supported by + implementers of the API. + + + **Note:** An alternative to `X-Forwarded-For` is defined in [RFC + 7239](https://tools.ietf.org/html/rfc7239). However, to this point RFC + 7239 is less-used and supported than `X-Forwarded-For`. + FSPIOP-Source: + name: FSPIOP-Source + in: header + schema: + type: string + required: true + description: >- + The `FSPIOP-Source` header field is a non-HTTP standard field used by + the API for identifying the sender of the HTTP request. The field should + be set by the original sender of the request. Required for routing and + signature verification (see header field `FSPIOP-Signature`). + FSPIOP-Destination: + name: FSPIOP-Destination + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-Destination` header field is a non-HTTP standard field used + by the API for HTTP header based routing of requests and responses to + the destination. The field must be set by the original sender of the + request if the destination is known (valid for all services except GET + /parties) so that any entities between the client and the server do not + need to parse the payload for routing purposes. If the destination is + not known (valid for service GET /parties), the field should be left + empty. + FSPIOP-Encryption: + name: FSPIOP-Encryption + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-Encryption` header field is a non-HTTP standard field used + by the API for applying end-to-end encryption of the request. + FSPIOP-Signature: + name: FSPIOP-Signature + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-Signature` header field is a non-HTTP standard field used by + the API for applying an end-to-end request signature. + FSPIOP-URI: + name: FSPIOP-URI + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-URI` header field is a non-HTTP standard field used by the + API for signature verification, should contain the service URI. Required + if signature verification is used, for more information, see [the API + Signature + document](https://github.com/mojaloop/docs/tree/master/Specification%20Document%20Set). + FSPIOP-HTTP-Method: + name: FSPIOP-HTTP-Method + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-HTTP-Method` header field is a non-HTTP standard field used + by the API for signature verification, should contain the service HTTP + method. Required if signature verification is used, for more + information, see [the API Signature + document](https://github.com/mojaloop/docs/tree/master/Specification%20Document%20Set). + Accept: + name: Accept + in: header + required: true + schema: + type: string + description: >- + The `Accept` header field indicates the version of the API the client + would like the server to use. + Content-Length: + name: Content-Length + in: header + required: false + schema: + type: integer + description: >- + The `Content-Length` header field indicates the anticipated size of the + payload body. Only sent if there is a body. + + + **Note:** The API supports a maximum size of 5242880 bytes (5 + Megabytes). + SubId: + name: SubId + in: path + required: true + schema: + type: string + description: >- + A sub-identifier of the party identifier, or a sub-type of the party + identifier's type. For example, `PASSPORT`, `DRIVING_LICENSE`. + responses: + '200': + description: OK + '202': + description: Accepted + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '405': + description: Method Not Allowed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '406': + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '501': + description: Not Implemented + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '503': + description: Service Unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + headers: + Content-Length: + required: false + schema: + type: integer + description: >- + The `Content-Length` header field indicates the anticipated size of the + payload body. Only sent if there is a body. + + + **Note:** The API supports a maximum size of 5242880 bytes (5 + Megabytes). + Content-Type: + schema: + type: string + required: true + description: >- + The `Content-Type` header indicates the specific version of the API used + to send the payload body. \ No newline at end of file diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/callback_map.json b/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/callback_map.json new file mode 100644 index 00000000..7b3f005f --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/callback_map.json @@ -0,0 +1,541 @@ +{ + "/transfers": { + "post": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/transfers/{ID}", + "pathPattern": "/transfers/{$request.body.transferId}", + "headerOverride": { + "FSPIOP-Source": "{$request.body.payeeFsp}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + }, + "bodyOverride": { + "completedTimestamp": "{$function.generic.curDateISO}", + "transferState": "COMMITTED", + "extensionList": null + } + }, + "errorCallback": { + "method": "put", + "path": "/transfers/{ID}/error", + "pathPattern": "/transfers/{$request.body.transferId}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/transfers/{ID}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/transfers/{ID}", + "pathPattern": "/transfers/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/transfers/{ID}/error", + "pathPattern": "/transfers/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/quotes": { + "post": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/quotes/{ID}", + "pathPattern": "/quotes/{$request.body.quoteId}", + "headerOverride": { + "FSPIOP-Source": "{$request.body.payee.partyIdInfo.fspId}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + }, + "bodyOverride": { + "transferAmount": { + "currency": "{$request.body.amount.currency}", + "amount": "{$request.body.amount.amount}" + }, + "expiration": "2040-01-01T01:01:01.001Z", + "extensionList": null + } + }, + "errorCallback": { + "method": "put", + "path": "/quotes/{ID}/error", + "pathPattern": "/quotes/{$request.body.quoteId}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/quotes/{ID}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/quotes/{ID}", + "pathPattern": "/quotes/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/quotes/{ID}/error", + "pathPattern": "/quotes/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/parties/{Type}/{ID}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/parties/{Type}/{ID}", + "pathPattern": "/parties/{$request.params.Type}/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + }, + "bodyOverride": { + "party": { + "partyIdInfo": { + "partyIdType": "{$request.params.Type}", + "partyIdentifier": "{$request.params.ID}", + "fspId": "{$config.FSPID}" + } + } + } + }, + "errorCallback": { + "method": "put", + "path": "/parties/{Type}/{ID}/error", + "pathPattern": "/parties/{$request.params.Type}/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/parties/{Type}/{ID}/{SubId}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/parties/{Type}/{ID}/{SubId}", + "pathPattern": "/parties/{$request.params.Type}/{$request.params.ID}/{$request.params.SubId}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + }, + "bodyOverride": { + "party": { + "partyIdInfo": { + "partyIdType": "{$request.params.Type}", + "partyIdentifier": "{$request.params.ID}", + "partySubIdOrType": null, + "fspId": "{$config.FSPID}" + } + } + } + }, + "errorCallback": { + "method": "put", + "path": "/parties/{Type}/{ID}/{SubId}/error", + "pathPattern": "/parties/{$request.params.Type}/{$request.params.ID}/{$request.params.SubId}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/transactionRequests": { + "post": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/transactionRequests/{ID}", + "pathPattern": "/transactionRequests/{$request.body.transactionRequestId}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/transactionRequests/{ID}/error", + "pathPattern": "/transactionRequests/{$request.body.transactionRequestId}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/transactionRequests/{ID}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/transactionRequests/{ID}", + "pathPattern": "/transactionRequests/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/transactionRequests/{ID}/error", + "pathPattern": "/transactionRequests/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/participants": { + "post": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/participants/{ID}", + "pathPattern": "/participants/{$request.body.partyList[0].partyIdentifier}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/participants/{ID}/error", + "pathPattern": "/participants/{$request.body.partyList[0].partyIdentifier}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/participants/{Type}/{ID}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}/error", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + }, + "post": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}/error", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + }, + "delete": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}/error", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/participants/{Type}/{ID}/{SubId}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}/{SubId}", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}/{SubId}/error", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + }, + "post": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}/{SubId}", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/participants/{Type}/{ID}/{SubId}/error", + "pathPattern": "/participants/{$request.params.Type}/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/transactions/{ID}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/transactions/{ID}", + "pathPattern": "/transactions/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/transactions/{ID}/error", + "pathPattern": "/transactions/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/bulkQuotes": { + "post": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/bulkQuotes/{ID}", + "pathPattern": "/bulkQuotes/{$request.body.bulkQuoteId}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/bulkQuotes/{ID}/error", + "pathPattern": "/bulkQuotes/{$request.body.bulkQuoteId}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/bulkQuotes/{ID}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/bulkQuotes/{ID}", + "pathPattern": "/bulkQuotes/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/bulkQuotes/{ID}/error", + "pathPattern": "/bulkQuotes/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/bulkTransfers": { + "post": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/bulkTransfers/{ID}", + "pathPattern": "/bulkTransfers/{$request.body.bulkTransferId}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/bulkTransfers/{ID}/error", + "pathPattern": "/bulkTransfers/{$request.body.bulkTransferId}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + }, + "/bulkTransfers/{ID}": { + "get": { + "fspid": "{$request.headers.fspiop-source}", + "successCallback": { + "method": "put", + "path": "/bulkTransfers/{ID}", + "pathPattern": "/bulkTransfers/{$request.params.ID}", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + }, + "errorCallback": { + "method": "put", + "path": "/bulkTransfers/{ID}/error", + "pathPattern": "/bulkTransfers/{$request.params.ID}/error", + "headerOverride": { + "FSPIOP-Source": "{$config.FSPID}", + "FSPIOP-Destination": "{$request.headers.fspiop-source}", + "Content-Type": "{$session.negotiatedContentType}", + "Date": "{$request.headers.date}" + } + } + } + } +} diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/mockRef.json b/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/mockRef.json new file mode 100644 index 00000000..39181171 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/mockRef.json @@ -0,0 +1,83 @@ +[ + { + "id": "party.personalInfo.complexName.firstName", + "pattern": "John|David|Michael|Chris|Mike|Mark|Paul|Daniel|James|Maria" + }, + { + "id": "party.personalInfo.complexName.middleName", + "pattern": "G|P|N|S" + }, + { + "id": "party.personalInfo.complexName.lastName", + "pattern": "Smith|Jones|Johnson|Lee|Brown|Williams|Rodriguez|Garcia|Gonzalez|Lopez" + }, + { + "id": "party.personalInfo.dateOfBirth", + "pattern": "^(19)\\d\\d[-](0[1-9]|1[012])[-](0[1-9]|[12][0-9]|2[0-8])$" + }, + { + "id": "transferId", + "faker": "internet.email" + }, + { + "id": "transferState", + "pattern": "COMMITTED|RESERVED|ABORTED|RECEIVED" + }, + { + "id": "fulfilment", + "pattern": "[A-Fa-f0-9]{64}" + }, + { + "id": "condition", + "pattern": "[A-Fa-f0-9]{64}" + }, + { + "id": "ilpPacket", + "pattern": "[A-Fa-f0-9]{256}" + }, + { + "id": "transferAmount.currency", + "pattern": "USD" + }, + { + "id": "transferAmount.amount", + "pattern": "123" + }, + { + "id": "payeeReceiveAmount.currency", + "pattern": "USD" + }, + { + "id": "payeeReceiveAmount.amount", + "pattern": "123" + }, + { + "id": "payeeFspFee.currency", + "pattern": "USD" + }, + { + "id": "payeeFspFee.amount", + "pattern": "2" + }, + { + "id": "payeeFspCommission.currency", + "pattern": "USD" + }, + { + "id": "payeeFspCommission.amount", + "pattern": "3" + }, + + { + "id": "errorInformation.errorCode", + "pattern": "600[1-9]" + }, + { + "id": "errorInformation.errorDescription", + "pattern": "This is a mock error description" + }, + { + "id": "Content-Length", + "pattern": "123" + } +] \ No newline at end of file diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/trigger_templates/transaction_request_followup.json b/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/trigger_templates/transaction_request_followup.json new file mode 100644 index 00000000..77ba26f3 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/fspiop_1.1/trigger_templates/transaction_request_followup.json @@ -0,0 +1,125 @@ +{ + "name": "Transaction Request Service Followup", + "inputValues": { + "payerFirstName": "Vijay", + "payerLastName": "Kumar", + "payerDOB": "1984-01-01", + "accept": "application/vnd.interoperability.parties+json;version=1.0", + "contentType": "application/vnd.interoperability.parties+json;version=1.0", + "transactionId": "e8c4572c-0826-22f4-aa3e-f5bbe928afa6", + "TrsNote": "note", + "TrsCurrency": "USD", + "TrsAmount": "100", + "TrsPayerIdType": "MSISDN", + "TrsPayerIdValue": "44123456789", + "TrsPayerFspId": "testingtoolkitdfsp", + "TrsPayeeIdType": "MSISDN", + "TrsPayeeIdValue": "9876543210", + "TrsPayeeFspId": "userdfsp", + "TrsScenario": "DEPOSIT", + "TrsInitiator": "PAYEE", + "TrsInitiatorType": "CONSUMER" + }, + "test_cases": [ + { + "id": 1, + "name": "Transaction Request Followup", + "requests": [ + { + "id": 2, + "description": "Get quote", + "apiVersion": { + "minorVersion": 0, + "majorVersion": 1, + "type": "fspiop", + "asynchronous": true + }, + "operationPath": "/quotes", + "method": "post", + "headers": { + "Accept": "{$inputs.accept}", + "Content-Type": "{$inputs.contentType}", + "Date": "{$function.generic.curDate}", + "FSPIOP-Source": "{$inputs.TrsPayerFspId}" + }, + "body": { + "quoteId": "{$function.generic.generateUUID}", + "transactionId": "{$inputs.transactionId}", + "payer": { + "partyIdInfo": { + "partyIdType": "{$inputs.TrsPayerIdType}", + "partyIdentifier": "{$inputs.TrsPayerIdValue}", + "fspId": "{$inputs.TrsPayerFspId}" + }, + "personalInfo": { + "complexName": { + "firstName": "{$inputs.payerFirstName}", + "lastName": "{$inputs.payerLastName}" + }, + "dateOfBirth": "{$inputs.payerDOB}" + } + }, + "payee": { + "partyIdInfo": { + "partyIdType": "{$inputs.TrsPayeeIdType}", + "partyIdentifier": "{$inputs.TrsPayeeIdValue}", + "fspId": "{$inputs.TrsPayeeFspId}" + } + }, + "amountType": "SEND", + "amount": { + "amount": "{$inputs.TrsAmount}", + "currency": "{$inputs.TrsCurrency}" + }, + "transactionType": { + "scenario": "{$inputs.TrsScenario}", + "initiator": "{$inputs.TrsInitiator}", + "initiatorType": "{$inputs.TrsInitiatorType}" + }, + "note": "{$inputs.TrsNote}" + }, + "tests": { + "assertions": [] + }, + "params": { + "Type": "", + "ID": "" + } + }, + { + "id": 3, + "description": "Send transfer", + "apiVersion": { + "minorVersion": 0, + "majorVersion": 1, + "type": "fspiop", + "asynchronous": true + }, + "operationPath": "/transfers", + "method": "post", + "headers": { + "Accept": "{$inputs.accept}", + "Content-Type": "{$inputs.contentType}", + "Date": "{$function.generic.curDate}", + "FSPIOP-Source": "{$inputs.TrsPayerFspId}" + }, + "body": { + "transferId": "{$prev.2.request.body.transactionId}", + "payerFsp": "{$inputs.TrsPayerFspId}", + "payeeFsp": "{$inputs.TrsPayeeFspId}", + "amount": { + "amount": "{$inputs.TrsAmount}", + "currency": "{$inputs.TrsCurrency}" + }, + "expiration": "{$prev.2.callback.body.expiration}", + "ilpPacket": "{$prev.2.callback.body.ilpPacket}", + "condition": "{$prev.2.callback.body.condition}" + }, + "tests": { + "assertions": [] + } + } + ] + } + ] +} diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/api_spec.yaml b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/api_spec.yaml new file mode 100644 index 00000000..98d8bd04 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/api_spec.yaml @@ -0,0 +1,1224 @@ +openapi: 3.0.2 +info: + title: Mojaloop PISP/Switch API + version: '1.0' + description: >- + A Mojaloop API for thirdparty interactions between `PISPs` (Payment + Initiation Service Providers) and a Mojaloop Switch. + license: + name: TBD + url: TBD +servers: + - url: / +paths: + '/consents/{ID}': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + get: + description: > + The HTTP request `GET /consents/{ID}` is used to get information + regarding a consent object created or requested earlier. The `{ID}` in + the URI should contain the `{ID}` that was used in the `POST /consents`. + summary: GetConsent + tags: + - consents + operationId: GetConsent + summary: GetConsent + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + patch: + description: > + The HTTP request `PATCH /consents/{ID}` is used + + + - In account linking in the Credential Registration phase. Used by a + DFSP + to notify a PISP a credential has been verified and registered with an + Auth service. + + - In account unlinking by a hub hosted auth service and by DFSPs + in non-hub hosted scenarios to notify participants of a consent being revoked. + + Called by a `auth-service` to notify a PISP and DFSP of consent status in hub hosted scenario. + Called by a `DFSP` to notify a PISP of consent status in non-hub hosted scenario. + tags: + - consents + - sampled + operationId: PatchConsentByID + summary: PatchConsentByID + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ConsentsIDPatchResponseVerified' + - $ref: '#/components/schemas/ConsentsIDPatchResponseRevoked' + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: > + The HTTP request `PUT /consents/{ID}` is used by the PISP and Auth + Service. + + + - Called by a `PISP` to after signing a challenge. Sent to an DFSP for + verification. + + - Called by a `auth-service` to notify a DFSP that a credential has been + verified and registered. + tags: + - consents + - sampled + operationId: PutConsentByID + summary: PutConsentByID + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ConsentsIDPutResponseSigned' + - $ref: '#/components/schemas/ConsentsIDPutResponseVerified' + parameters: + - $ref: '#/components/parameters/Content-Length' + responses: + '200': + $ref: '#/components/responses/200' + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + delete: + description: > + The HTTP request `DELETE /consents/{ID}` is used to mark as deleted a + previously created consent. + + + - Called by a PISP when a user wants to remove their consent. + operationId: DeleteConsentByID + parameters: + - $ref: '#/components/parameters/Accept' + tags: + - consents + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/consents/{ID}/error': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + put: + tags: + - consents + operationId: NotifyErrorConsents + summary: NotifyErrorConsents + description: > + DFSP responds to the PISP if something went wrong with validating or + storing consent. + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Error information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/participants/{Type}/{ID}': + parameters: + - $ref: '#/components/parameters/Type' + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + post: + description: >- + The HTTP request `POST /participants/{Type}/{ID}` (or `POST + /participants/{Type}/{ID}/{SubId}`) is used to create information in the + server regarding the provided identity, defined by `{Type}`, `{ID}`, and + optionally `{SubId}` (for example, `POST /participants/MSISDN/123456789` + or `POST /participants/BUSINESS/shoecompany/employee1`). An + ExtensionList element has been added to this reqeust in version v1.1 + summary: Create participant information + tags: + - participants + operationId: ParticipantsByIDAndType + parameters: + - $ref: '#/components/parameters/Accept' + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Participant information to be created. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantsTypeIDSubIDPostRequest' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + get: + description: >- + The HTTP request `GET /participants/{Type}/{ID}` (or `GET + /participants/{Type}/{ID}/{SubId}`) is used to find out in which FSP the + requested Party, defined by `{Type}`, `{ID}` and optionally `{SubId}`, + is located (for example, `GET /participants/MSISDN/123456789`, or `GET + /participants/BUSINESS/shoecompany/employee1`). This HTTP request should + support a query string for filtering of currency. To use filtering of + currency, the HTTP request `GET /participants/{Type}/{ID}?currency=XYZ` + should be used, where `XYZ` is the requested currency. + summary: Look up participant information + tags: + - participants + operationId: ParticipantsByTypeAndID + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + put: + description: >- + The callback `PUT /participants/{Type}/{ID}` (or `PUT + /participants/{Type}/{ID}/{SubId}`) is used to inform the client of a + successful result of the lookup, creation, or deletion of the FSP + information related to the Party. If the FSP information is deleted, the + fspId element should be empty; otherwise the element should include the + FSP information for the Party. + summary: Return participant information + tags: + - participants + operationId: ParticipantsByTypeAndID3 + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Participant information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantsTypeIDPutResponse' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + delete: + description: >- + The HTTP request `DELETE /participants/{Type}/{ID}` (or `DELETE + /participants/{Type}/{ID}/{SubId}`) is used to delete information in the + server regarding the provided identity, defined by `{Type}` and `{ID}`) + (for example, `DELETE /participants/MSISDN/123456789`), and optionally + `{SubId}`. This HTTP request should support a query string to delete FSP + information regarding a specific currency only. To delete a specific + currency only, the HTTP request `DELETE + /participants/{Type}/{ID}?currency=XYZ` should be used, where `XYZ` is + the requested currency. + + + **Note:** The Account Lookup System should verify that it is the Party’s + current FSP that is deleting the FSP information. + summary: Delete participant information + tags: + - participants + operationId: ParticipantsByTypeAndID2 + parameters: + - $ref: '#/components/parameters/Accept' + responses: + '202': + $ref: '#/components/responses/202' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' +components: + parameters: + ID: + name: ID + in: path + required: true + schema: + type: string + description: The identifier value. + Content-Type: + name: Content-Type + in: header + schema: + type: string + required: true + description: >- + The `Content-Type` header indicates the specific version of the API used + to send the payload body. + Date: + name: Date + in: header + schema: + type: string + required: true + description: The `Date` header field indicates the date when the request was sent. + X-Forwarded-For: + name: X-Forwarded-For + in: header + schema: + type: string + required: false + description: >- + The `X-Forwarded-For` header field is an unofficially accepted standard + used for informational purposes of the originating client IP address, as + a request might pass multiple proxies, firewalls, and so on. Multiple + `X-Forwarded-For` values should be expected and supported by + implementers of the API. + + + **Note:** An alternative to `X-Forwarded-For` is defined in [RFC + 7239](https://tools.ietf.org/html/rfc7239). However, to this point RFC + 7239 is less-used and supported than `X-Forwarded-For`. + FSPIOP-Source: + name: FSPIOP-Source + in: header + schema: + type: string + required: true + description: >- + The `FSPIOP-Source` header field is a non-HTTP standard field used by + the API for identifying the sender of the HTTP request. The field should + be set by the original sender of the request. Required for routing and + signature verification (see header field `FSPIOP-Signature`). + FSPIOP-Destination: + name: FSPIOP-Destination + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-Destination` header field is a non-HTTP standard field used + by the API for HTTP header based routing of requests and responses to + the destination. The field must be set by the original sender of the + request if the destination is known (valid for all services except GET + /parties) so that any entities between the client and the server do not + need to parse the payload for routing purposes. If the destination is + not known (valid for service GET /parties), the field should be left + empty. + FSPIOP-Encryption: + name: FSPIOP-Encryption + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-Encryption` header field is a non-HTTP standard field used + by the API for applying end-to-end encryption of the request. + FSPIOP-Signature: + name: FSPIOP-Signature + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-Signature` header field is a non-HTTP standard field used by + the API for applying an end-to-end request signature. + FSPIOP-URI: + name: FSPIOP-URI + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-URI` header field is a non-HTTP standard field used by the + API for signature verification, should contain the service URI. Required + if signature verification is used, for more information, see [the API + Signature + document](https://github.com/mojaloop/docs/tree/master/Specification%20Document%20Set). + FSPIOP-HTTP-Method: + name: FSPIOP-HTTP-Method + in: header + schema: + type: string + required: false + description: >- + The `FSPIOP-HTTP-Method` header field is a non-HTTP standard field used + by the API for signature verification, should contain the service HTTP + method. Required if signature verification is used, for more + information, see [the API Signature + document](https://github.com/mojaloop/docs/tree/master/Specification%20Document%20Set). + Accept: + name: Accept + in: header + required: true + schema: + type: string + description: >- + The `Accept` header field indicates the version of the API the client + would like the server to use. + Content-Length: + name: Content-Length + in: header + required: false + schema: + type: integer + description: >- + The `Content-Length` header field indicates the anticipated size of the + payload body. Only sent if there is a body. + + + **Note:** The API supports a maximum size of 5242880 bytes (5 + Megabytes). + Type: + name: Type + in: path + required: true + schema: + type: string + description: 'The type of the party identifier. For example, `MSISDN`, `PERSONAL_ID`.' + responses: + '200': + description: OK + '202': + description: Accepted + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '405': + description: Method Not Allowed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '406': + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '501': + description: Not Implemented + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + '503': + description: Service Unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationResponse' + headers: + Content-Length: + $ref: '#/components/headers/Content-Length' + Content-Type: + $ref: '#/components/headers/Content-Type' + headers: + Content-Length: + required: false + schema: + type: integer + description: >- + The `Content-Length` header field indicates the anticipated size of the + payload body. Only sent if there is a body. + + + **Note:** The API supports a maximum size of 5242880 bytes (5 + Megabytes). + Content-Type: + schema: + type: string + required: true + description: >- + The `Content-Type` header indicates the specific version of the API used + to send the payload body. + schemas: + ErrorCode: + title: ErrorCode + type: string + pattern: '^[1-9]\d{3}$' + description: >- + The API data type ErrorCode is a JSON String of four characters, + consisting of digits only. Negative numbers are not allowed. A leading + zero is not allowed. Each error code in the API is a four-digit number, + for example, 1234, where the first number (1 in the example) represents + the high-level error category, the second number (2 in the example) + represents the low-level error category, and the last two numbers (34 in + the example) represent the specific error. + example: '5100' + ErrorDescription: + title: ErrorDescription + type: string + minLength: 1 + maxLength: 128 + description: Error description string. + ExtensionKey: + title: ExtensionKey + type: string + minLength: 1 + maxLength: 32 + description: Extension key. + ExtensionValue: + title: ExtensionValue + type: string + minLength: 1 + maxLength: 128 + description: Extension value. + Extension: + title: Extension + type: object + description: Data model for the complex type Extension. + properties: + key: + $ref: '#/components/schemas/ExtensionKey' + value: + $ref: '#/components/schemas/ExtensionValue' + required: + - key + - value + ExtensionList: + title: ExtensionList + type: object + description: >- + Data model for the complex type ExtensionList. An optional list of + extensions, specific to deployment. + properties: + extension: + type: array + items: + $ref: '#/components/schemas/Extension' + minItems: 1 + maxItems: 16 + description: Number of Extension elements. + required: + - extension + ErrorInformation: + title: ErrorInformation + type: object + description: Data model for the complex type ErrorInformation. + properties: + errorCode: + $ref: '#/components/schemas/ErrorCode' + errorDescription: + $ref: '#/components/schemas/ErrorDescription' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - errorCode + - errorDescription + ErrorInformationResponse: + title: ErrorInformationResponse + type: object + description: >- + Data model for the complex type object that contains an optional element + ErrorInformation used along with 4xx and 5xx responses. + properties: + errorInformation: + $ref: '#/components/schemas/ErrorInformation' + AccountId: + title: AccountId + type: string + description: > + A long-lived unique account identifier provided by the DFSP. This MUST + NOT + + be Bank Account Number or anything that may expose a User's private bank + + account information. + pattern: '^([0-9A-Za-z_~\-\.]+[0-9A-Za-z_~\-])$' + minLength: 1 + maxLength: 1023 + ConsentScopeType: + title: ConsentScopeType + type: string + enum: + - accounts.getBalance + - accounts.transfer + description: | + The scopes requested for a ConsentRequest. + - "accounts.getBalance" - Get the balance of a given account. + - "accounts.transfer" - Initiate a transfer from an account. + Scope: + title: Scope + type: object + description: Scope + Account Identifier mapping for a Consent. + example: | + { + accountId: "dfsp.username.5678", + actions: [ "accounts.transfer", "accounts.getBalance" ] + } + properties: + accountId: + $ref: '#/components/schemas/AccountId' + actions: + type: array + items: + $ref: '#/components/schemas/ConsentScopeType' + required: + - accountId + - actions + CredentialType: + title: CredentialType + type: string + enum: + - FIDO + description: | + The type of the Credential. + - "FIDO" - A FIDO public/private keypair + FIDOPublicKeyCredential: + title: FIDOPublicKeyCredential + type: object + description: > + An object sent in a `PUT /consents/{ID}` request. + + Based on https://w3c.github.io/webauthn/#iface-pkcredential + + and mostly on: https://webauthn.guide/#registration + + AuthenticatorAttestationResponse + + https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-attestationobject + properties: + id: + type: string + description: | + credential id: identifier of pair of keys, base64 encoded + https://w3c.github.io/webauthn/#ref-for-dom-credential-id + minLength: 59 + maxLength: 118 + rawId: + type: string + description: | + raw credential id: identifier of pair of keys, base64 encoded + minLength: 59 + maxLength: 118 + response: + type: object + description: | + AuthenticatorAttestationResponse + properties: + clientDataJSON: + type: string + description: | + JSON string with client data + minLength: 121 + maxLength: 512 + attestationObject: + type: string + description: | + CBOR.encoded attestation object + minLength: 306 + maxLength: 2048 + required: + - clientDataJSON + - attestationObject + additionalProperties: false + type: + type: string + description: 'response type, we need only the type of public-key' + enum: + - public-key + required: + - id + - rawId + - response + - type + additionalProperties: false + SignedCredential: + title: SignedCredential + type: object + description: > + A credential used to allow a user to prove their identity and access + + to an account with a DFSP. + + + SignedCredential is a special formatting of the credential to allow us + to be + + more explicit about the `status` field - it should only ever be PENDING + when + + updating a credential. + properties: + credentialType: + $ref: '#/components/schemas/CredentialType' + status: + type: string + enum: + - PENDING + description: The challenge has signed but not yet verified. + payload: + $ref: '#/components/schemas/FIDOPublicKeyCredential' + required: + - credentialType + - status + - payload + additionalProperties: false + ConsentsIDPutResponseSigned: + title: ConsentsIDPutResponseSigned + type: object + description: > + The HTTP request `PUT /consents/{ID}` is used by the PISP to update a + Consent with a signed challenge and register a credential. + + Called by a `PISP` to after signing a challenge. Sent to a DFSP for + verification. + properties: + scopes: + type: array + items: + $ref: '#/components/schemas/Scope' + credential: + $ref: '#/components/schemas/SignedCredential' + required: + - scopes + - credential + additionalProperties: false + VerifiedCredential: + title: VerifiedCredential + type: object + description: > + A credential used to allow a user to prove their identity and access + + to an account with a DFSP. + + + VerifiedCredential is a special formatting of the credential to allow us + to be + + more explicit about the `status` field - it should only ever be VERIFIED + when + + updating a credential. + properties: + credentialType: + $ref: '#/components/schemas/CredentialType' + status: + type: string + enum: + - VERIFIED + description: 'The Credential is valid, and ready to be used by the PISP.' + payload: + $ref: '#/components/schemas/FIDOPublicKeyCredential' + required: + - credentialType + - status + - payload + additionalProperties: false + ConsentsIDPutResponseVerified: + title: ConsentsIDPutResponseVerified + type: object + description: > + The HTTP request `PUT /consents/{ID}` is used by the DFSP or + Auth-Service to update a Consent object once it has been Verified. + + Called by a `auth-service` to notify a DFSP that a credential has been + verified and registered. + properties: + scopes: + type: array + items: + $ref: '#/components/schemas/Scope' + credential: + $ref: '#/components/schemas/VerifiedCredential' + required: + - scopes + - credential + additionalProperties: false + ConsentStatusTypeVerified: + title: ConsentStatusType + type: string + enum: + - VERIFIED + description: | + The status of the Consent. + - "VERIFIED" - The Consent is valid and verified. + ConsentsIDPatchResponseVerified: + title: ConsentsIDPatchResponseVerified + description: | + PATCH /consents/{ID} request object. + + Sent by the DFSP to the PISP when a consent is verified. + Used in the "Register Credential" part of the Account linking flow. + type: object + properties: + credential: + type: object + properties: + status: + $ref: '#/components/schemas/ConsentStatusTypeVerified' + required: + - status + required: + - credential + ConsentStatusTypeRevoked: + title: ConsentStatusType + type: string + enum: + - REVOKED + description: | + The status of the Consent. + - "REVOKED" - The Consent is no longer valid and has been revoked. + DateTime: + title: DateTime + type: string + pattern: >- + ^(?:[1-9]\d{3}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)-02-29)T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:(\.\d{3}))(?:Z|[+-][01]\d:[0-5]\d)$ + description: >- + The API data type DateTime is a JSON String in a lexical format that is + restricted by a regular expression for interoperability reasons. The + format is according to [ISO + 8601](https://www.iso.org/iso-8601-date-and-time-format.html), expressed + in a combined date, time and time zone format. A more readable version + of the format is yyyy-MM-ddTHH:mm:ss.SSS[-HH:MM]. Examples are + "2016-05-24T08:38:08.699-04:00", "2016-05-24T08:38:08.699Z" (where Z + indicates Zulu time zone, same as UTC). + example: '2016-05-24T08:38:08.699-04:00' + ConsentsIDPatchResponseRevoked: + title: ConsentsIDPatchResponseRevoked + description: | + PATCH /consents/{ID} request object. + + Sent to both the PISP and DFSP when a consent is revoked. + Used in the "Unlinking" part of the Account Unlinking flow. + type: object + properties: + status: + $ref: '#/components/schemas/ConsentStatusTypeRevoked' + revokedAt: + $ref: '#/components/schemas/DateTime' + required: + - status + - revokedAt + ErrorInformationObject: + title: ErrorInformationObject + type: object + description: Data model for the complex type object that contains ErrorInformation. + properties: + errorInformation: + $ref: '#/components/schemas/ErrorInformation' + required: + - errorInformation + FspId: + title: FspId + type: string + minLength: 1 + maxLength: 32 + description: FSP identifier. + ParticipantsTypeIDPutResponse: + title: ParticipantsTypeIDPutResponse + type: object + description: >- + The object sent in the PUT /participants/{Type}/{ID}/{SubId} and + /participants/{Type}/{ID} callbacks. + properties: + fspId: + $ref: '#/components/schemas/FspId' + Currency: + title: Currency + description: >- + The currency codes defined in [ISO + 4217](https://www.iso.org/iso-4217-currency-codes.html) as three-letter + alphabetic codes are used as the standard naming representation for + currencies. + type: string + minLength: 3 + maxLength: 3 + enum: + - AED + - AFN + - ALL + - AMD + - ANG + - AOA + - ARS + - AUD + - AWG + - AZN + - BAM + - BBD + - BDT + - BGN + - BHD + - BIF + - BMD + - BND + - BOB + - BRL + - BSD + - BTN + - BWP + - BYN + - BZD + - CAD + - CDF + - CHF + - CLP + - CNY + - COP + - CRC + - CUC + - CUP + - CVE + - CZK + - DJF + - DKK + - DOP + - DZD + - EGP + - ERN + - ETB + - EUR + - FJD + - FKP + - GBP + - GEL + - GGP + - GHS + - GIP + - GMD + - GNF + - GTQ + - GYD + - HKD + - HNL + - HRK + - HTG + - HUF + - IDR + - ILS + - IMP + - INR + - IQD + - IRR + - ISK + - JEP + - JMD + - JOD + - JPY + - KES + - KGS + - KHR + - KMF + - KPW + - KRW + - KWD + - KYD + - KZT + - LAK + - LBP + - LKR + - LRD + - LSL + - LYD + - MAD + - MDL + - MGA + - MKD + - MMK + - MNT + - MOP + - MRO + - MUR + - MVR + - MWK + - MXN + - MYR + - MZN + - NAD + - NGN + - NIO + - NOK + - NPR + - NZD + - OMR + - PAB + - PEN + - PGK + - PHP + - PKR + - PLN + - PYG + - QAR + - RON + - RSD + - RUB + - RWF + - SAR + - SBD + - SCR + - SDG + - SEK + - SGD + - SHP + - SLL + - SOS + - SPL + - SRD + - STD + - SVC + - SYP + - SZL + - THB + - TJS + - TMT + - TND + - TOP + - TRY + - TTD + - TVD + - TWD + - TZS + - UAH + - UGX + - USD + - UYU + - UZS + - VEF + - VND + - VUV + - WST + - XAF + - XCD + - XDR + - XOF + - XPF + - YER + - ZAR + - ZMW + - ZWD + ParticipantsTypeIDSubIDPostRequest: + title: ParticipantsTypeIDSubIDPostRequest + type: object + description: >- + The object sent in the POST /participants/{Type}/{ID}/{SubId} and + /participants/{Type}/{ID} requests. An additional optional ExtensionList + element has been added as part of v1.1 changes. + properties: + fspId: + $ref: '#/components/schemas/FspId' + currency: + $ref: '#/components/schemas/Currency' + extensionList: + $ref: '#/components/schemas/ExtensionList' + required: + - fspId diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/callback_map.json b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/callback_map.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/callback_map.json @@ -0,0 +1 @@ +{} diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/thirdparty-pisp-api-template.yaml b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/thirdparty-pisp-api-template.yaml new file mode 100644 index 00000000..a822864d --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/thirdparty-pisp-api-template.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.2 +info: + title: Mojaloop PISP/Switch API + version: '1.0' + description: A Mojaloop API for thirdparty interactions between `PISPs` (Payment Initiation Service Providers) and a Mojaloop Switch. + license: + name: TBD + url: TBD +servers: + - url: / +paths: + # Account Linking Flow + # TTK acting as DFSP receiving a callback for a POST /consents request + /consents/{ID}: + $ref: '../../../../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/consents_ID.yaml' + /consents/{ID}/error: + $ref: '../../../../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/consents_ID_error.yaml' + # TTK acting as the ALS + /participants/{Type}/{ID}: + $ref: '../../../../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/participants_Type_ID.yaml' + + # Transfer Verification Flow + # to be implemented + # /thirdpartyRequests/verifications/{ID}: + # $ref: '../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/thirdpartyRequests_verifications_ID.yaml' + # /thirdpartyRequests/verifications/{ID}/error: + # $ref: '../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/thirdpartyRequests_verifications_ID_error.yaml' diff --git a/docker/ml-testing-toolkit/spec_files/reports/templates/newman/html_template.html b/docker/ml-testing-toolkit/spec_files/reports/templates/newman/html_template.html new file mode 100644 index 00000000..34bed92c --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/reports/templates/newman/html_template.html @@ -0,0 +1,1074 @@ + + {{!-- {{#each items}} + + + {{name}} + + + $ {{price}} + + + {{/each}} + + + + Total: ${{total items}} + + --}} + + + + + + + + + Testing Toolkit Assertions Report + + + + + + + + +
+
+ + + +
+
+
+ +
+
+
+
+

Testing Toolkit Report

+
{{runtimeInformation.completedTime}}
+
+
+
+
+
+ +
+
Total Assertions
+

{{totalAssertions test_cases}}

+
+
+
+
+
+
+
+ +
+
Total Passed Tests
+

{{totalPassedAssertions test_cases}}

+
+
+
+
+
+
+
+ +
+
Total Failed Tests
+

{{totalFailedAssertions test_cases}}

+
+
+
+
+
+
+
+
+
+
+
+
Runtime Information
+ Template Name: {{name}}
+ {{#if runtimeInformation}} + Total run duration: {{runtimeInformation.runDurationMs}} ms
+ Average response time: {{runtimeInformation.avgResponseTime}}
+ {{/if}} +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Summary ItemTotalFailed
Test Cases{{totalTestCases test_cases}}{{failedTestCases test_cases}}
Requests{{totalRequests test_cases}}{{failedRequests test_cases}}
Assertions{{totalAssertions test_cases}}{{totalFailedAssertions test_cases}}
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + +
+ + + + +
+ +
+ +
+
+ {{#each test_cases}} +
+ + +
+ {{#each requests}} +
+
+
+
+ {{#if (ifAllTestsPassedInRequest request)}} +
+ {{else}} + +
+
+
+
+
+
+
+
Request Information
+ Request Method: {{request.method}}
+ Request URL: {{request.path}}
+
+
+
+
+
Response Information
+ Response Code: {{response.status}} - {{response.statusText}}
+ Mean time per request: NA
+ Mean size per request: NA
+
+
Test Pass Percentage
+
+
+ {{#if (ifAllTestsPassedInRequest request)}} +
+ {{else}} +
+ {{/if}} +
{{testPassPercentage request.tests}} %
+
+
+
+
+
+
+
+
+ {{#if request.headers}} +
+
+
+
+
+
Request Headers
+
+ + + + {{#each request.headers}} + + + + + {{/each}} + +
Header NameHeader Value
{{@key}}{{this}}
+
+
+
+
+
+
+ {{/if}} + {{#if request.body}} +
+
+
+
+
+
Request Body
+
+
{{jsonStringify request.body}}
+
+ +
+
+
+
+
+ {{/if}} + {{#if additionalInfo.curlRequest}} +
+
+
+
+
+
CURL command
+
+
{{additionalInfo.curlRequest}}
+
+ +
+
+
+
+
+ {{/if}} + {{#if response.data}} +
+
+
+
+
+
Response Body
+
+
{{jsonStringify response.data}}
+
+ +
+
+
+
+
+ {{/if}} + {{#if callback.headers}} +
+
+
+
+
+
Callback Headers
+
+ + + + {{#each callback.headers}} + + + + + {{/each}} + +
Header NameHeader Value
{{@key}}{{this}}
+
+
+
+
+
+
+ {{/if}} + {{#if callback.body}} +
+
+
+
+
+
Callback Body
+
+
{{jsonStringify callback.body}}
+
+ +
+
+
+
+
+ {{/if}} + +
+
+
+
Test Information
+
+ + + + {{#each request.tests.assertions}} + + + {{#if (isAssertionPassed resultStatus.status)}} + + {{else}} + + + {{/if}} + + {{/each}} + + + + + + + +
NameResult
{{description}} + PASSED + + FAILED +
({{resultStatus.message}})
Total{{request.tests.passedAssertionsCount}} / {{request.tests.assertions.length}}
+
+
+
+
+
+
+
Test Failure
+
+ + + + +
Test NameAssertion Error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{/each}} +
+ +
+ {{/each}} +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docker/ml-testing-toolkit/spec_files/reports/templates/newman/pdf_template.html b/docker/ml-testing-toolkit/spec_files/reports/templates/newman/pdf_template.html new file mode 100644 index 00000000..c5976bc8 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/reports/templates/newman/pdf_template.html @@ -0,0 +1,765 @@ + + {{!-- {{#each items}} + + + {{name}} + + + $ {{price}} + + + {{/each}} + + + + Total: ${{total items}} + + --}} + + + + + + + + + Testing Toolkit Assertions Report + + + + + + + + +
+
+
+ +
+
+
+
+


+

Testing Toolkit Report

+
{{runtimeInformation.completedTime}}
+


+
+
+
+
+
+ +
+
Total Assertions
+

{{totalAssertions test_cases}}

+
+
+
+
+
+
+
+ +
+
Total Passed Tests
+

{{totalPassedAssertions test_cases}}

+
+
+
+
+
+
+
+ +
+
Total Failed Tests
+

{{totalFailedAssertions test_cases}}

+
+
+
+
+
+
+
+


+
+
+
+
+
Runtime Information
+ Template Name: {{name}}
+ {{#if runtimeInformation}} + Total run duration: {{runtimeInformation.runDurationMs}} ms
+ Average response time: {{runtimeInformation.avgResponseTime}}
+ {{/if}} +
+
+
+
+


+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Summary ItemTotalFailed
Test Cases{{totalTestCases test_cases}}{{failedTestCases test_cases}}
Requests{{totalRequests test_cases}}{{failedRequests test_cases}}
Assertions{{totalAssertions test_cases}}{{totalFailedAssertions test_cases}}
+
+
+
+
+
+
+
+
+
+

















+


+
+ + +
+ +
+
+ {{#each test_cases}} +
+ + +
+ {{#each requests}} +
+
+
+
+ {{#if (ifAllTestsPassedInRequest request)}} +
+ {{else}} + +
+
+
+
+
+
+
+
Request Information
+ Request Method: {{request.method}}
+ Request URL: {{request.path}}
+
+
+
+
+
Response Information
+ Response Code: {{response.status}} - {{response.statusText}}
+ Mean time per request: NA
+ Mean size per request: NA
+
+
Test Pass Percentage
+
+
+ {{#if (ifAllTestsPassedInRequest request)}} +
+ {{else}} +
+ {{/if}} +
{{testPassPercentage request.tests}} %
+
+
+
+
+
+
+
+
+ {{#if request.headers}} +
+
+
+
+
+
Request Headers
+
+ + + + {{#each request.headers}} + + + + + {{/each}} + +
Header NameHeader Value
{{@key}}{{this}}
+
+
+
+
+
+
+ {{/if}} + {{#if request.body}} +
+
+
+
+
+
Request Body
+
+
{{jsonStringify request.body}}
+
+
+
+
+
+
+ {{/if}} + {{#if response.data}} +
+
+
+
+
+
Response Body
+
+
{{jsonStringify response.data}}
+
+
+
+
+
+
+ {{/if}} + {{#if callback.headers}} +
+
+
+
+
+
Callback Headers
+
+ + + + {{#each callback.headers}} + + + + + {{/each}} + +
Header NameHeader Value
{{@key}}{{this}}
+
+
+
+
+
+
+ {{/if}} + {{#if callback.body}} +
+
+
+
+
+
Callback Body
+
+
{{jsonStringify callback.body}}
+
+
+
+
+
+
+ {{/if}} + +
+
+
+
Test Information
+
+ + + + {{#each request.tests.assertions}} + + + {{#if (isAssertionPassed resultStatus.status)}} + + {{else}} + + + {{/if}} + + {{/each}} + + + + + + + +
NameResult
{{description}} + PASSED + + FAILED +
({{resultStatus.message}})
Total{{request.tests.passedAssertionsCount}} / {{request.tests.assertions.length}}
+
+
+
+
+
+
+
Test Failure
+
+ + + + +
Test NameAssertion Error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{/each}} +
+ +
+ {{/each}} +
+
+
+
+
+ + + + + + + diff --git a/docker/ml-testing-toolkit/spec_files/reports/templates/newman/script.js b/docker/ml-testing-toolkit/spec_files/reports/templates/newman/script.js new file mode 100644 index 00000000..f4bc6c0b --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/reports/templates/newman/script.js @@ -0,0 +1,98 @@ +function now () { + return new Date().toLocaleDateString() +} + +function totalAssertions (testCases) { + return testCases.reduce((total, curTestCase) => { + const assertionsInRequest = curTestCase.requests.reduce((assertionCountRequest, curRequest) => { + return assertionCountRequest + ((curRequest.request.tests && curRequest.request.tests.assertions) ? curRequest.request.tests.assertions.length : 0) + }, 0) + return total + assertionsInRequest + }, 0) +} + +function totalPassedAssertions (testCases) { + return testCases.reduce((total, curTestCase) => { + const assertionsInRequest = curTestCase.requests.reduce((assertionCountRequest, curRequest) => { + return assertionCountRequest + ((curRequest.request.tests && curRequest.request.tests.assertions) ? curRequest.request.tests.assertions.length : 0) + }, 0) + const passedAssertionsInRequest = curTestCase.requests.reduce((passedAssertionCountRequest, curRequest) => { + return passedAssertionCountRequest + ((curRequest.request.tests && curRequest.request.tests.passedAssertionsCount) ? curRequest.request.tests.passedAssertionsCount : 0) + }, 0) + return total + passedAssertionsInRequest + }, 0) +} + +function totalFailedAssertions (testCases) { + return totalAssertions(testCases) - totalPassedAssertions(testCases) +} + +function totalTestCases (testCases) { + return testCases.length +} + +function failedTestCases (testCases) { + return testCases.reduce((total, curTestCase) => { + const assertionsInRequest = curTestCase.requests.reduce((assertionCountRequest, curRequest) => { + return assertionCountRequest + ((curRequest.request.tests && curRequest.request.tests.assertions) ? curRequest.request.tests.assertions.length : 0) + }, 0) + const passedAssertionsInRequest = curTestCase.requests.reduce((passedAssertionCountRequest, curRequest) => { + return passedAssertionCountRequest + ((curRequest.request.tests && curRequest.request.tests.passedAssertionsCount) ? curRequest.request.tests.passedAssertionsCount : 0) + }, 0) + return total + (passedAssertionsInRequest === assertionsInRequest ? 0 : 1) + }, 0) +} + +function totalRequests (testCases) { + return testCases.reduce((total, curTestCase) => { + return total + curTestCase.requests.length + }, 0) +} + +function failedRequests (testCases) { + return testCases.reduce((total, curTestCase) => { + const faileRequestsCount = curTestCase.requests.reduce((failedRequestCountTemp, curRequest) => { + return failedRequestCountTemp + ((curRequest.request.tests && curRequest.request.tests.assertions) ? (curRequest.request.tests.assertions.length === curRequest.request.tests.passedAssertionsCount ? 0 : 1) : 0) + }, 0) + return total + faileRequestsCount + }, 0) +} + +function testPassPercentage (tests) { + if (tests && tests.assertions) { + return Math.round(tests.passedAssertionsCount * 100 / tests.assertions.length) + } else { + return 0 + } +} + +function ifAllTestsPassedInRequest (request) { + if (request.tests && request.tests.assertions) { + return request.tests.passedAssertionsCount === request.tests.assertions.length + } else { + return false + } +} + +function ifFailedTestCase (testCase) { + const failedRequest = testCase.requests.find((item) => { + if (item.request.tests.assertions) { + return item.request.tests.passedAssertionsCount !== item.request.tests.assertions.length + } else { + return false + } + }) + if (failedRequest) { + return true + } else { + return false + } +} + +function jsonStringify (inputObject) { + return JSON.stringify(inputObject, null, 2) +} + +function isAssertionPassed (status) { + return status === 'SUCCESS' +} diff --git a/docker/ml-testing-toolkit/spec_files/rules_callback/config.json b/docker/ml-testing-toolkit/spec_files/rules_callback/config.json new file mode 100644 index 00000000..08320e43 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/rules_callback/config.json @@ -0,0 +1,3 @@ +{ + "activeRulesFile": "default.json" +} diff --git a/docker/ml-testing-toolkit/spec_files/rules_callback/default.json b/docker/ml-testing-toolkit/spec_files/rules_callback/default.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/rules_callback/default.json @@ -0,0 +1 @@ +[] diff --git a/docker/ml-testing-toolkit/spec_files/rules_response/config.json b/docker/ml-testing-toolkit/spec_files/rules_response/config.json new file mode 100644 index 00000000..599ca8fc --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/rules_response/config.json @@ -0,0 +1,3 @@ +{ + "activeRulesFile": "default.json" +} \ No newline at end of file diff --git a/docker/ml-testing-toolkit/spec_files/rules_response/default.json b/docker/ml-testing-toolkit/spec_files/rules_response/default.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/rules_response/default.json @@ -0,0 +1 @@ +[] diff --git a/docker/ml-testing-toolkit/spec_files/rules_validation/config.json b/docker/ml-testing-toolkit/spec_files/rules_validation/config.json new file mode 100644 index 00000000..08320e43 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/rules_validation/config.json @@ -0,0 +1,3 @@ +{ + "activeRulesFile": "default.json" +} diff --git a/docker/ml-testing-toolkit/spec_files/rules_validation/default.json b/docker/ml-testing-toolkit/spec_files/rules_validation/default.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/rules_validation/default.json @@ -0,0 +1 @@ +[] diff --git a/docker/ml-testing-toolkit/spec_files/system_config.json b/docker/ml-testing-toolkit/spec_files/system_config.json new file mode 100644 index 00000000..51baee33 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/system_config.json @@ -0,0 +1,45 @@ +{ + "API_PORT": 5000, + "HOSTING_ENABLED": false, + "CONFIG_VERSIONS": { + "response": 1, + "callback": 1, + "validation": 1, + "forward": 1, + "userSettings": 1 + }, + "DB": { + "URI": "mongodb://mongo:27017/dfsps" + }, + "OAUTH": { + "AUTH_ENABLED": false, + "APP_OAUTH_CLIENT_KEY": "asdf", + "APP_OAUTH_CLIENT_SECRET": "asdf", + "MTA_ROLE": "Application/MTA", + "PTA_ROLE": "Application/PTA", + "EVERYONE_ROLE": "Internal/everyone", + "OAUTH2_ISSUER": "http://ml-testing-toolkit:5050/api/oauth2/token", + "JWT_COOKIE_NAME": "TTK-API_ACCESS_TOKEN", + "EMBEDDED_CERTIFICATE": "password" + }, + "CONNECTION_MANAGER": { + "API_URL": "http://connection-manager-api:5061", + "AUTH_ENABLED": false, + "HUB_USERNAME": "hub", + "HUB_PASSWORD": "hub" + }, + "API_DEFINITIONS": [ + { + "type": "fspiop", + "version": "1.1", + "folderPath": "fspiop_1.1", + "asynchronous": true + }, + { + "type": "thirdparty_pisp", + "version": "0.1", + "folderPath": "thirdparty_pisp", + "asynchronous": true + } + ] +} \ No newline at end of file diff --git a/docker/ml-testing-toolkit/spec_files/user_config.json b/docker/ml-testing-toolkit/spec_files/user_config.json new file mode 100644 index 00000000..76e00cf3 --- /dev/null +++ b/docker/ml-testing-toolkit/spec_files/user_config.json @@ -0,0 +1,64 @@ +{ + "VERSION": 1, + "CALLBACK_ENDPOINT": "http://inbound-thirdparty-scheme-adapter:4005", + "CALLBACK_RESOURCE_ENDPOINTS": { + "enabled": true, + "endpoints": [ + { + "method": "put", + "path": "/parties/{Type}/{ID}", + "endpoint": "http://inbound-sdk-scheme-adapter:7000" + }, + { + "method": "put", + "path": "/quotes/{ID}", + "endpoint": "http://inbound-sdk-scheme-adapter:7000" + }, + { + "method": "put", + "path": "/authorizations/{ID}", + "endpoint": "http://inbound-sdk-scheme-adapter:7000" + }, + { + "method": "patch", + "path": "/thirdpartyRequests/transactions/{ID}", + "endpoint": "http://inbound-thirdparty-scheme-adapter:4005" + }, + { + "method": "put", + "path": "/thirdpartyRequests/transactions/{ID}/error", + "endpoint": "http://inbound-thirdparty-scheme-adapter:4005" + }, + { + "method": "put", + "path": "/transfers/{ID}", + "endpoint": "http://inbound-sdk-scheme-adapter:7000" + } + ] + }, + "HUB_ONLY_MODE": false, + "ENDPOINTS_DFSP_WISE": { + "dfsps": {} + }, + "SEND_CALLBACK_ENABLE": true, + "FSPID": "switch", + "DEFAULT_USER_FSPID": "userdfsp", + "TRANSFERS_VALIDATION_WITH_PREVIOUS_QUOTES": false, + "TRANSFERS_VALIDATION_ILP_PACKET": false, + "TRANSFERS_VALIDATION_CONDITION": false, + "ILP_SECRET": "secret", + "VERSIONING_SUPPORT_ENABLE": true, + "OVERRIDE_WITH_ENV": true, + "VALIDATE_INBOUND_JWS": false, + "VALIDATE_INBOUND_PUT_PARTIES_JWS": false, + "JWS_SIGN": false, + "JWS_SIGN_PUT_PARTIES": false, + "CONNECTION_MANAGER_API_URL": "http://connection-manager-api:5061", + "CONNECTION_MANAGER_AUTH_ENABLED": false, + "CONNECTION_MANAGER_HUB_USERNAME": "hub", + "CONNECTION_MANAGER_HUB_PASSWORD": "hub", + "INBOUND_MUTUAL_TLS_ENABLED": false, + "OUTBOUND_MUTUAL_TLS_ENABLED": false, + "ADVANCED_FEATURES_ENABLED": true, + "CALLBACK_TIMEOUT": 20000 +} diff --git a/package-lock.json b/package-lock.json index 2c480b3c..7e3a5944 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2544,11 +2544,15 @@ "@types/istanbul-lib-report": "*" } }, + "@types/javascript-state-machine": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/javascript-state-machine/-/javascript-state-machine-2.4.2.tgz", + "integrity": "sha512-GarmWnkw8FIVbduC8T06BHrWHR4tw9ypHMEkArEt9af87VGWRKuMfYFXpA29MzHfUgofjVVjJcbtWNx3nL9Xcg==" + }, "@types/jest": { - "version": "26.0.23", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz", - "integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==", - "dev": true, + "version": "26.0.20", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz", + "integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==", "requires": { "jest-diff": "^26.0.0", "pretty-format": "^26.0.0" @@ -2591,9 +2595,9 @@ "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==" }, "@types/node": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.0.0.tgz", - "integrity": "sha512-TmCW5HoZ2o2/z2EYi109jLqIaPIi9y/lc2LmDCWzuCi35bcaQ+OtUh6nwBiFK7SOu25FAU5+YKdqFZUwtqGSdg==" + "version": "14.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", + "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -2618,12 +2622,34 @@ "integrity": "sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA==", "dev": true }, + "@types/promise-timeout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/promise-timeout/-/promise-timeout-1.3.0.tgz", + "integrity": "sha512-AtVKSZUtpBoZ4SshXJk5JcTXJllinHKKx615lsRNJUsbbFlI0AI8drlnoiQ+PNvjkeoF9Y8fJUh6UO2khsIBZw==" + }, "@types/rc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@types/rc/-/rc-1.1.0.tgz", "integrity": "sha512-qw1q31xPnaeExbOA1daA3nfeKW2uZQN4Xg8QqZDM3vsXPHK/lyDpjWXJQIcrByRDcBzZJ3ccchSMMTDtCWgFpA==", "dev": true }, + "@types/redis": { + "version": "2.8.31", + "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.31.tgz", + "integrity": "sha512-daWrrTDYaa5iSDFbgzZ9gOOzyp2AJmYK59OlG/2KGBgYWF3lfs8GDKm1c//tik5Uc93hDD36O+qLPvzDolChbA==", + "requires": { + "@types/node": "*" + } + }, + "@types/redis-mock": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@types/redis-mock/-/redis-mock-0.17.0.tgz", + "integrity": "sha512-UDKHu9otOSE1fPjgn0H7UoggqVyuRYfo3WJpdXdVmzgGmr1XIM/dTk/gRYf/bLjIK5mxpV8inA5uNBS2sVOilA==", + "dev": true, + "requires": { + "@types/redis": "*" + } + }, "@types/shot": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/shot/-/shot-4.0.0.tgz", @@ -2648,7 +2674,6 @@ "version": "15.0.5", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", - "dev": true, "requires": { "@types/yargs-parser": "*" } @@ -3552,12 +3577,6 @@ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==" }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, "is-ci": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", @@ -3825,16 +3844,6 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, "bintrees": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", @@ -4416,14 +4425,6 @@ "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.5.0" - }, - "dependencies": { - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - } } }, "chownr": { @@ -5516,6 +5517,11 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "denque": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", + "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -5556,8 +5562,7 @@ "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==" }, "dir-glob": { "version": "3.0.1", @@ -7009,13 +7014,6 @@ "flat-cache": "^3.0.4" } }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7370,10 +7368,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "optional": true }, "fstream": { @@ -8833,6 +8830,11 @@ "istanbul-lib-report": "^3.0.0" } }, + "javascript-state-machine": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/javascript-state-machine/-/javascript-state-machine-3.1.0.tgz", + "integrity": "sha512-BwhYxQ1OPenBPXC735RgfB+ZUG8H3kjsx8hrYTgWnoy6TPipEy4fiicyhT2lxRKAXq9pG7CfFT8a2HLr6Hmwxg==" + }, "jest": { "version": "26.0.1", "resolved": "https://registry.npmjs.org/jest/-/jest-26.0.1.tgz", @@ -9822,7 +9824,6 @@ "dev": true, "optional": true, "requires": { - "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -10764,7 +10765,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, "requires": { "chalk": "^4.0.0", "diff-sequences": "^26.6.2", @@ -10877,8 +10877,7 @@ "jest-get-type": { "version": "26.3.0", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==" }, "jest-haste-map": { "version": "26.3.0", @@ -11271,6 +11270,12 @@ "@types/node": "*" } }, + "jest-mock-process": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jest-mock-process/-/jest-mock-process-1.4.1.tgz", + "integrity": "sha512-ZZUKRlEBizutngoO4KngzN30YoeAYP3nnwimk4cpi9WqLxQUf6SlAPK5p1D9usEpxDS3Uif2MIez3Bq0vGYR+g==", + "dev": true + }, "jest-pnp-resolver": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", @@ -11703,6 +11708,11 @@ "supports-color": "^7.0.0" } }, + "jgexml": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jgexml/-/jgexml-0.4.4.tgz", + "integrity": "sha512-j0AzSWT7LXy3s3i1cdv5NZxUtscocwiBxgOLiEBfitCehm8STdXVrcOlbAWsJFLCq1elZYpQlGqA9k8Z+n9iJA==" + }, "jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -15475,7 +15485,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, "requires": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", @@ -15487,7 +15496,6 @@ "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", @@ -15499,14 +15507,12 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -15515,7 +15521,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -15523,14 +15528,12 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" } } }, @@ -15574,6 +15577,11 @@ "retry": "^0.12.0" } }, + "promise-timeout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/promise-timeout/-/promise-timeout-1.3.0.tgz", + "integrity": "sha512-5yANTE0tmi5++POym6OgtFmwfDvOXABD9oj/jLQr5GPEyuNEb7jH4wbbANJceJid49jwhi1RddxnhnEAb/doqg==" + }, "prompts": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", @@ -15894,6 +15902,41 @@ "strip-indent": "^3.0.0" } }, + "redis": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", + "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", + "requires": { + "denque": "^1.5.0", + "redis-commands": "^1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + } + }, + "redis-commands": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", + "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" + }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + }, + "redis-mock": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.52.0.tgz", + "integrity": "sha512-s+6m2BFfgbXdbN4Pbv/+y7/lLd5M7gCTuYI0EAdb0IqvqDjYoexh+fs3Gd1w2bYIchNAez11wDhbBlFxXnjUog==", + "dev": true + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } + }, "reftools": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.8.tgz", @@ -18832,11 +18875,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, - "jgexml": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/jgexml/-/jgexml-0.4.4.tgz", - "integrity": "sha512-j0AzSWT7LXy3s3i1cdv5NZxUtscocwiBxgOLiEBfitCehm8STdXVrcOlbAWsJFLCq1elZYpQlGqA9k8Z+n9iJA==" - }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", diff --git a/package.json b/package.json index 4c7c0ae8..92f295db 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "audit:check": "SHELL=sh check-audit --production", "build": "npm run build:openapi; npm run build:dto; tsc -p ./tsconfig.build.json", "build:openapi": "openapi bundle --output ./src/interface/api.yaml --ext yaml ./src/interface/api-template.yaml", + "build:ttk-3p:api": "openapi bundle --output ./docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/api_spec.yaml --ext yaml ./docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/thirdparty-pisp-api-template.yaml", "build:dto": "openapi-typescript ./src/interface/api.yaml --output ./src/interface/openapi.d.ts; npm run lint -- --fix ./src/interface/openapi.d.ts", "validate:api": "swagger-cli validate ./src/interface/api.yaml", "docker:build": "docker build -t auth-service:local -f ./Dockerfile ./", "docker:run": "docker run -p 4004:4004 auth-service:local", "lint": "eslint ./src/**/*.ts *.js", + "lint:fix": "eslint --fix ./src/**/*.ts *.js", "pretest": "echo \"pretest - TODO...\"", "release": "standard-version --releaseCommitMessageFormat 'chore(release): {{currentTag}} [skip ci]'", "standard": "echo '\\033[1;33m This project uses eslint instead of standard. Use `npm run lint` instead.'", @@ -77,9 +79,8 @@ "@types/hapi__hapi": "^20.0.8", "@types/hapi__inert": "^5.2.2", "@types/hapi__vision": "^5.5.2", - "@types/jest": "26.0.23", - "@types/node": "^16.0.0", "@types/rc": "^1.1.0", + "@types/redis-mock": "^0.17.0", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", "add": "^2.0.6", @@ -98,11 +99,13 @@ "jest": "26.0.1", "jest-cucumber": "^2.0.11", "jest-junit": "10.0.0", + "jest-mock-process": "^1.4.1", "lint-staged": "^11.0.0", "multi-file-swagger": "^2.3.0", "nodemon": "^2.0.9", "npm-audit-resolver": "2.3.1", "npm-check-updates": "11.8.1", + "redis-mock": "0.52.0", "source-map-support": "0.5.19", "standard-version": "^9.3.0", "swagger-cli": "^4.0.4", @@ -118,13 +121,18 @@ "@hapi/vision": "^6.1.0", "@mojaloop/api-snippets": "^12.4.5", "@mojaloop/central-services-error-handling": "11.3.0", + "@mojaloop/central-services-health": "^13.0.0", "@mojaloop/central-services-logger": "10.6.1", "@mojaloop/central-services-metrics": "11.0.0", - "@mojaloop/event-sdk": "10.7.1", - "@mojaloop/central-services-health": "^13.0.0", "@mojaloop/central-services-shared": "^13.0.5", + "@mojaloop/event-sdk": "10.7.1", "@mojaloop/sdk-standard-components": "^15.12.0", "@types/convict": "^6.1.1", + "@types/javascript-state-machine": "^2.4.2", + "@types/jest": "26.0.20", + "@types/node": "^14.14.30", + "@types/promise-timeout": "^1.3.0", + "@types/redis": "^2.8.28", "@types/uuid": "^8.3.1", "ajv": "8.6.0", "ajv-keywords": "5.0.0", @@ -138,12 +146,15 @@ "hapi-swagger": "^14.2.1", "knex": "^0.21.19", "mysql": "^2.18.1", + "javascript-state-machine": "^3.1.0", "npm-run-all": "^4.1.5", "openapi-response-validator": "^9.1.0", "openapi-typescript": "^4.0.1", "parse-strings-in-object": "^2.0.0", "path": "^0.12.7", + "promise-timeout": "^1.3.0", "rc": "^1.2.8", + "redis": "^3.1.2", "sqlite3": "^5.0.2", "ts-node": "^10.0.0", "uuid": "^8.3.2" diff --git a/src/domain/auth-payload.ts b/src/domain/auth-payload.ts index 435f7377..d657d460 100644 --- a/src/domain/auth-payload.ts +++ b/src/domain/auth-payload.ts @@ -1,5 +1,5 @@ import { Consent } from '../model/consent' -import { Scope } from '../model/scope' +import { ModelScope } from '../model/scope' /* * Interface for incoming payload @@ -33,10 +33,10 @@ export function hasActiveCredentialForPayload (consent: Consent): boolean { * Domain function to check for matching Consent scope */ export function hasMatchingScopeForPayload ( - consentScopes: Scope[], + consentScopes: ModelScope[], payload: AuthPayload): boolean { // Check if any scope matches - return consentScopes.some((scope: Scope): boolean => + return consentScopes.some((scope: ModelScope): boolean => scope.accountId === payload.sourceAccountId ) } diff --git a/src/domain/authorizations.ts b/src/domain/authorizations.ts index 30f1cb1d..31a91e15 100644 --- a/src/domain/authorizations.ts +++ b/src/domain/authorizations.ts @@ -34,7 +34,7 @@ -------------- ******/ import { Consent } from '../model/consent' -import { Scope } from '../model/scope' +import { ModelScope } from '../model/scope' import { consentDB, scopeDB } from '~/model/db' import { verifySignature } from '~/domain/challenge' import { @@ -81,7 +81,7 @@ export async function validateAndVerifySignature ( throw new DatabaseError(payload.consentId) } - let consentScopes: Scope[] + let consentScopes: ModelScope[] // Retrieve scopes for the consent try { diff --git a/src/domain/consents.ts b/src/domain/consents.ts index 1bd2380f..2eee32db 100644 --- a/src/domain/consents.ts +++ b/src/domain/consents.ts @@ -35,7 +35,7 @@ ******/ import { insertConsentWithScopes } from '../model/db' -import { Scope } from '../model/scope' +import { ModelScope } from '../model/scope' import { Consent, ConsentCredential } from '../model/consent' import { logger } from '~/shared/logger' import { convertThirdpartyScopesToDatabaseScope } from './scopes' @@ -83,7 +83,7 @@ export async function createAndStoreConsent ( clientDataJSON: credential.payload.response.clientDataJSON } - const scopes: Scope[] = convertThirdpartyScopesToDatabaseScope(thirdpartyScopes, consentId) + const scopes: ModelScope[] = convertThirdpartyScopesToDatabaseScope(thirdpartyScopes, consentId) try { await insertConsentWithScopes(consent, scopes) diff --git a/src/domain/consents/ID.ts b/src/domain/consents/ID.ts index 78635789..6e73a083 100644 --- a/src/domain/consents/ID.ts +++ b/src/domain/consents/ID.ts @@ -36,14 +36,14 @@ -------------- ******/ import { Consent, ConsentCredential } from '~/model/consent' -import { Scope } from '~/model/scope' +import { ModelScope } from '~/model/scope' import { consentDB, scopeDB } from '~/model/db' import { ChallengeMismatchError, IncorrectConsentStatusError, EmptyCredentialPayloadError } from '../errors' -import { convertDatabaseScopesToThirdpartyScopes } from '~/domain/scopes' +import { convertModelScopesToThirdpartyScopes } from '~/domain/scopes' import { CredentialStatusEnum } from '~/model/consent/consent' import { thirdparty as tpAPI } from '@mojaloop/api-snippets'; @@ -85,8 +85,8 @@ export async function buildConsentsIDPutResponseVerifiedBody ( ): Promise { /* Retrieve the scopes pertinent to this consentId and populate the scopes accordingly. */ - const scopes: Scope[] = await scopeDB.retrieveAll(consent.id) - const TPScopes: tpAPI.Schemas.Scope[] = convertDatabaseScopesToThirdpartyScopes(scopes) + const scopes: ModelScope[] = await scopeDB.retrieveAll(consent.id) + const TPScopes: tpAPI.Schemas.Scope[] = convertModelScopesToThirdpartyScopes(scopes) const consentBody: tpAPI.Schemas.ConsentsIDPutResponseVerified = { scopes: TPScopes, @@ -95,7 +95,7 @@ export async function buildConsentsIDPutResponseVerifiedBody ( status: CredentialStatusEnum.VERIFIED, payload: { id: consent.credentialId!, - rawId: 'TODO: figure out what goes here', + rawId: consent.credentialId!, response: { clientDataJSON: consent.clientDataJSON!, attestationObject: consent.attestationObject! diff --git a/src/domain/scopes.ts b/src/domain/scopes.ts index 90a88936..9101af7d 100644 --- a/src/domain/scopes.ts +++ b/src/domain/scopes.ts @@ -35,7 +35,7 @@ -------------- ******/ -import { Scope } from '../model/scope' +import { ModelScope } from '../model/scope' import { thirdparty as tpAPI } from '@mojaloop/api-snippets' /** @@ -43,12 +43,12 @@ import { thirdparty as tpAPI } from '@mojaloop/api-snippets' * scope & consent ids, and returns array of formatted scopes * @param scopes Scopes retrieved from database */ -export function convertDatabaseScopesToThirdpartyScopes ( - scopes: Scope[]): tpAPI.Schemas.Scope[] { +export function convertModelScopesToThirdpartyScopes ( + scopes: ModelScope[]): tpAPI.Schemas.Scope[] { // Dictionary of accountId to Thirdparty Scope object const scopeDictionary = {} - scopes.forEach((scope: Scope): void => { + scopes.forEach((scope: ModelScope): void => { const accountId: string = scope.accountId if (!(accountId in scopeDictionary)) { @@ -73,10 +73,10 @@ export function convertDatabaseScopesToThirdpartyScopes ( * @param consentId Id of Consent to which scopes belong */ export function convertThirdpartyScopesToDatabaseScope ( - thirdpartyScopes: tpAPI.Schemas.Scope[], consentId: string): Scope[] { - const scopes: Scope[] = thirdpartyScopes.map( - (element: tpAPI.Schemas.Scope): Scope[] => - element.actions.map((action: string): Scope => ({ + thirdpartyScopes: tpAPI.Schemas.Scope[], consentId: string): ModelScope[] { + const scopes: ModelScope[] = thirdpartyScopes.map( + (element: tpAPI.Schemas.Scope): ModelScope[] => + element.actions.map((action: string): ModelScope => ({ consentId, accountId: element.accountId, action diff --git a/src/model/db.ts b/src/model/db.ts index c78ced0f..2fae8f5b 100644 --- a/src/model/db.ts +++ b/src/model/db.ts @@ -30,7 +30,7 @@ import Knex from 'knex' import Config from '../shared/config' import { Consent, ConsentDB } from './consent' -import { Scope, ScopeDB } from './scope' +import { ModelScope, ScopeDB } from './scope' const Db: Knex = Knex(Config.DATABASE) const consentDB: ConsentDB = new ConsentDB(Db) @@ -38,7 +38,7 @@ const scopeDB: ScopeDB = new ScopeDB(Db) const closeKnexConnection = async (): Promise => Db.destroy() -async function insertConsentWithScopes (consent: Consent, scopes: Scope[]): Promise { +async function insertConsentWithScopes (consent: Consent, scopes: ModelScope[]): Promise { const trxProvider = Db.transactionProvider() const trx = await trxProvider() try { diff --git a/src/model/persistent.model.ts b/src/model/persistent.model.ts new file mode 100644 index 00000000..899d5773 --- /dev/null +++ b/src/model/persistent.model.ts @@ -0,0 +1,275 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +// eslint-disable-next-line import/no-named-as-default +import StateMachine, { + Method, + StateMachineConfig, + StateMachineInterface, + TransitionEvent +} from 'javascript-state-machine' +import { KVS } from '~/shared/kvs' +import { Logger as SDKLogger } from '@mojaloop/sdk-standard-components' + +/** + * @interface ControlledStateMachine + * @description specialized state machine with init & error transitions + * and transition notification methods + */ +export interface ControlledStateMachine extends StateMachineInterface { + /** + * @method init + * @description all controlled state machines needs to be initiated + */ + init: Method + + /** + * @method error + * @description error or exceptions could happen in state machine life cycle, + * so we need a transition and corresponding state to reflect this + */ + error: Method + + /** + * @method onAfterTransition + * @description callback called after every transition + */ + onAfterTransition: Method; + + /** + * @method onPendingTransition + * @description callback called when transition is pending, + * before calling specialized transition handler + */ + onPendingTransition: Method; + onError: Method; +} + +/** + * @interface StateData + * @description data interface to represent the model's state data + */ +export interface StateData extends Record { + /** + * @property {StateType=string} currentState + * @description the model's current state value + */ + currentState: StateType +} + +/** + * @interface PersistentModelConfig + * @description config dependencies needed to deliver persistent model features +*/ +export interface PersistentModelConfig { + /** + * @property {string} key + * @description the key at which the model instance will be persisted + */ + key: string; + + /** + * @property {KVS} kvs + * @description Key-Value storage used to persist model + */ + kvs: KVS; + + /** + * @property {SDKLogger.Logger} logger + * @description used to send log events + */ + logger: SDKLogger.Logger; +} + +/** + * @class PersistentModel + * @description persistent model with defined workflow (life cycle) which operates on state data entity + * @param {ControlledStateMachine} JSM - type of state machine to handle and execute workflow + * @param {StateData} Data - type of state data needed by model + */ +export class PersistentModel { + /** + * @property {PersistentModelConfig} config + * @description specified model's dependencies + * declared readonly, because it should be only setup at construction + */ + protected readonly config: PersistentModelConfig + + /** + * @property {} fsm + * @description Final State Machine instance which handles/executes workflow's state transitions + */ + public readonly fsm: JSM + + /** + * @property {} data + * @description state data instance + */ + public data: Data + + /** + * @param {} data - initial state data + * @param {PersistentModelConfig} config - dependencies + * @param {StateMachineConfig} specOrig - state machine configuration which describes the model's workflow + */ + constructor ( + data: Data, + config: PersistentModelConfig, + specOrig: StateMachineConfig + ) { + // flat copy of parameters + this.data = { ...data } + this.config = { ...config } + const spec = { ...specOrig } + + // prepare transition methods + spec.methods = { + // inject basic methods here, so they can be overloaded by spec.methods + onAfterTransition: this.onAfterTransition.bind(this) as Method, + onPendingTransition: this.onPendingTransition.bind(this) as Method, + + // copy methods from received state machine specification + ...spec.methods + } + + // prepare transitions' specification + spec.transitions = [ + // inject error transition here, so it can be overloaded by spec.transitions + { name: 'error', from: '*', to: 'errored' }, + + // copy transitions from received state machine specification + ...spec.transitions + ] + + // propagate state from data.currentState, then spec.init and use 'none' as default + spec.init = (data.currentState || spec.init || 'none') as string + + // create a new state machine instance + this.fsm = new StateMachine(spec) as JSM + } + + // accessors to config properties + get logger (): SDKLogger.Logger { + return this.config.logger + } + + get key (): string { + return this.config.key + } + + get kvs (): KVS { + return this.config.kvs + } + + /** + * @method onAfterTransition + * @description called after every transition and updates data.currentState + * @param {TransitionEvent)} event - transition's event description + */ + async onAfterTransition (event: TransitionEvent): Promise { + this.logger.info(`State machine transitioned '${event.transition}': ${event.from} -> ${event.to}`) + // update internal state data + this.data.currentState = event.to + } + + /** + * @method onPendingTransition + * @description called when transition starts, + * it allows to call `error` transition even + * if there is a pending transition + * @param {string} transition - the name of the pending transition + */ + onPendingTransition (transition: string): void { + // allow transitions to 'error' state while other transitions are in progress + if (transition !== 'error') { + throw new Error(`Transition '${transition}' requested while another transition is in progress.`) + } + } + + /** + * @method saveToKVS + * @description stores model's state data in Key-Value Store + */ + async saveToKVS (): Promise { + try { + const res = await this.kvs.set(this.key, this.data) + this.logger.info({ res }) + this.logger.info(`Persisted model in cache: ${this.key}`) + } catch (err) { + this.logger.push({ err }) + this.logger.info(`Error saving model: ${this.key}`) + throw err + } + } +} + +/** + * @name create + * @description allows to create a new instance of persistent model + * @param {} data - initial model's state data + * @param {} config - model's configured dependencies + * @param {} spec - state machine configuration + * @returns {Promise>} persistent model instance + */ +export async function create ( + data: Data, + config: PersistentModelConfig, + spec: StateMachineConfig +): Promise > { + // create a new model + const model = new PersistentModel(data, config, spec) + + // enforce to finish any transition to state specified by data.currentState or spec.init + await model.fsm.state + return model +} + +/** + * @name loadFromKVS + * @description loads PersistentModel from KVS storage using given `config` and `spec` + * @param {PersistentModelConfig} config - model's configured dependencies - should be the same used with `create` + * @param {StateMachineConfig} spec - state machine configuration - should be the same used with `create` + * @returns {Promise >} persistent model instance loaded from KVS + */ +export async function loadFromKVS ( + config: PersistentModelConfig, + spec: StateMachineConfig +): Promise > { + try { + const data = await config.kvs.get(config.key) + if (!data) { + throw new Error(`No data found in KVS for: ${config.key}`) + } + config.logger.push({ data }) + config.logger.info('data loaded from KVS') + return new PersistentModel(data, config, spec) + } catch (err) { + config.logger.push({ err }) + config.logger.info(`Error loading data from KVS for key: ${config.key}`) + throw err + } +} diff --git a/src/model/scope/index.ts b/src/model/scope/index.ts index 7feacc1a..ce4427a8 100644 --- a/src/model/scope/index.ts +++ b/src/model/scope/index.ts @@ -28,6 +28,6 @@ ******/ export { - Scope, + ModelScope, ScopeDB } from './scope' diff --git a/src/model/scope/scope.ts b/src/model/scope/scope.ts index 016df82c..778f2298 100644 --- a/src/model/scope/scope.ts +++ b/src/model/scope/scope.ts @@ -40,10 +40,8 @@ import { NotFoundError } from '../errors' /* * Interface for Scope resource type -* TODO: rename this so it doesn't conflict with the Thirdparty API's -* Scope interface. Maybe something like AuthDBScope or ScopeDBRowObject */ -export interface Scope { +export interface ModelScope { id?: number; consentId: string; action: string; @@ -62,7 +60,7 @@ export class ScopeDB { } // Add a single Scope or an array of Scopes - public async insert (scopes: Scope | Scope[], trx?: Knex.Transaction): Promise { + public async insert (scopes: ModelScope | ModelScope[], trx?: Knex.Transaction): Promise { // To avoid inconsistencies between DBs, we define a standard // way to deal with empty arrays. // We just return true because an empty array was anyways @@ -71,7 +69,7 @@ export class ScopeDB { return true } - const action = this.Db('Scope').insert(scopes) + const action = this.Db('Scope').insert(scopes) if (trx) { await action.transacting(trx) } else { @@ -81,9 +79,9 @@ export class ScopeDB { } // Retrieve Scopes by Consent ID - public async retrieveAll (consentId: string): Promise { - const scopes: Scope[] = await this - .Db('Scope') + public async retrieveAll (consentId: string): Promise { + const scopes: ModelScope[] = await this + .Db('Scope') .select('*') .where({ consentId }) diff --git a/src/server/plugins/index.ts b/src/server/plugins/index.ts index e0506c87..527dbbba 100644 --- a/src/server/plugins/index.ts +++ b/src/server/plugins/index.ts @@ -32,11 +32,13 @@ import ErrorHandling from '@mojaloop/central-services-error-handling' import { Util } from '@mojaloop/central-services-shared' import Good from './good' import OpenAPI from './openAPI' +import { StatePlugin } from './state' async function register (server: Server): Promise { const openapiBackend = await OpenAPI.initialize() const plugins = [ + StatePlugin, Util.Hapi.OpenapiBackendValidator, Good, openapiBackend, diff --git a/src/server/plugins/state.ts b/src/server/plugins/state.ts new file mode 100644 index 00000000..e18991fe --- /dev/null +++ b/src/server/plugins/state.ts @@ -0,0 +1,135 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + + -------------- + ******/ + import { + MojaloopRequests, + ThirdpartyRequests, + Logger as SDKLogger +} from '@mojaloop/sdk-standard-components' +import { KVS } from '~/shared/kvs' +import { PubSub } from '~/shared/pub-sub' +import { ResponseToolkit, Server } from '@hapi/hapi' +import { RedisConnectionConfig } from '~/shared/redis-connection' +import { logger } from '~/shared/logger' + +import config from '~/shared/config' + +export interface StateResponseToolkit extends ResponseToolkit { + getKVS: () => KVS + getPublisher: () => PubSub + getSubscriber: () => PubSub + getLogger: () => SDKLogger.Logger + getMojaloopRequests: () => MojaloopRequests + getThirdpartyRequests: () => ThirdpartyRequests + getDFSPId: () => string +} + +export const StatePlugin = { + version: '1.0.0', + name: 'StatePlugin', + once: true, + + register: async (server: Server): Promise => { + // KVS & PubSub are using the same Redis instance + const connection: RedisConnectionConfig = { + host: config.REDIS.HOST, + port: config.REDIS.PORT, + timeout: config.REDIS.TIMEOUT, + logger + } + + // prepare redis connection instances + // once the client enters the subscribed state it is not supposed to issue + // any other commands, except for additional SUBSCRIBE, PSUBSCRIBE, + // UNSUBSCRIBE, PUNSUBSCRIBE, PING and QUIT commands. + // So we create two connections, one for subscribing another for + // and publishing + const kvs = new KVS(connection) + const publisher = new PubSub(connection) + const subscriber = new PubSub(connection) + + // prepare Requests instances + const mojaloopRequests = new MojaloopRequests({ + logger, + peerEndpoint: config.SHARED.PEER_ENDPOINT, + alsEndpoint: config.SHARED.ALS_ENDPOINT, + quotesEndpoint: config.SHARED.QUOTES_ENDPOINT, + transfersEndpoint: config.SHARED.TRANSFERS_ENDPOINT, + bulkTransfersEndpoint: config.SHARED.BULK_TRANSFERS_ENDPOINT, + servicesEndpoint: config.SHARED.SERVICES_ENDPOINT, + thirdpartyRequestsEndpoint: config.SHARED.THIRDPARTY_REQUESTS_ENDPOINT, + transactionRequestsEndpoint: config.SHARED.TRANSACTION_REQUEST_ENDPOINT, + dfspId: config.PARTICIPANT_ID, + tls: config.SHARED.TLS, + jwsSign: config.SHARED.JWS_SIGN, + jwsSigningKey: config.SHARED.JWS_SIGNING_KEY + }) + + const thirdpartyRequest = new ThirdpartyRequests({ + logger, + peerEndpoint: config.SHARED.PEER_ENDPOINT, + alsEndpoint: config.SHARED.ALS_ENDPOINT, + quotesEndpoint: config.SHARED.QUOTES_ENDPOINT, + transfersEndpoint: config.SHARED.TRANSFERS_ENDPOINT, + bulkTransfersEndpoint: config.SHARED.BULK_TRANSFERS_ENDPOINT, + servicesEndpoint: config.SHARED.SERVICES_ENDPOINT, + thirdpartyRequestsEndpoint: config.SHARED.THIRDPARTY_REQUESTS_ENDPOINT, + transactionRequestsEndpoint: config.SHARED.TRANSACTION_REQUEST_ENDPOINT, + dfspId: config.PARTICIPANT_ID, + tls: config.SHARED.TLS, + jwsSign: config.SHARED.JWS_SIGN, + jwsSigningKey: config.SHARED.JWS_SIGNING_KEY + }) + + + try { + // connect them all to Redis instance + await Promise.all([kvs.connect(), subscriber.connect(), publisher.connect()]) + logger.info(`StatePlugin: connecting KVS(${kvs.isConnected}) & + Publisher(${publisher.isConnected}) & Subscriber(${subscriber.isConnected}):`) + + // prepare toolkit accessors + server.decorate('toolkit', 'getKVS', (): KVS => kvs) + server.decorate('toolkit', 'getPublisher', (): PubSub => publisher) + server.decorate('toolkit', 'getSubscriber', (): PubSub => subscriber) + server.decorate('toolkit', 'getLogger', (): SDKLogger.Logger => logger) + server.decorate('toolkit', 'getMojaloopRequests', (): MojaloopRequests => mojaloopRequests) + server.decorate('toolkit', 'getThirdpartyRequests', (): ThirdpartyRequests => thirdpartyRequest) + server.decorate('toolkit', 'getDFSPId', (): string => config.PARTICIPANT_ID) + // disconnect from redis when server is stopped + server.events.on('stop', async () => { + await Promise.allSettled([kvs.disconnect(), publisher.disconnect(), subscriber.disconnect()]) + logger.info('StatePlugin: Server stopped -> disconnecting KVS & PubSub') + }) + } catch (err) { + logger.error('StatePlugin: unexpected exception during plugin registration') + logger.error(err) + logger.error('StatePlugin: exiting process') + process.exit(1) + } + } +} diff --git a/src/shared/config.ts b/src/shared/config.ts index cbc885de..f096c609 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -43,6 +43,11 @@ interface ServiceConfig { PARTICIPANT_ID: string; DATABASE: DatabaseConfig; ENV: string; + REDIS: { + HOST: string; + PORT: number; + TIMEOUT: number; + }; INSPECT: { DEPTH: number; SHOW_HIDDEN: boolean; @@ -53,6 +58,7 @@ interface ServiceConfig { ALS_ENDPOINT?: string; QUOTES_ENDPOINT?: string; TRANSFERS_ENDPOINT?: string; + SERVICES_ENDPOINT?: string; BULK_TRANSFERS_ENDPOINT?: string; THIRDPARTY_REQUESTS_ENDPOINT?: string; TRANSACTION_REQUEST_ENDPOINT?: string; @@ -108,6 +114,26 @@ const ConvictConfig = Convict({ env: 'PARTICIPANT_ID', arg: 'participantId' }, + REDIS: { + HOST: { + doc: 'The Redis Hostname/IP address to connect.', + format: '*', + default: 'localhost', + env: 'REDIS_HOST' + }, + PORT: { + doc: 'The Redis port to connect.', + format: 'port', + default: 6379, + env: 'REDIS_PORT' + }, + TIMEOUT: { + doc: 'The Redis connection timeout', + format: 'nat', + default: 100, + env: 'REDIS_TIMEOUT' + } + }, INSPECT: { DEPTH: { doc: 'Inspection depth', @@ -131,6 +157,7 @@ const ConvictConfig = Convict({ ALS_ENDPOINT: '0.0.0.0:4002', QUOTES_ENDPOINT: '0.0.0.0:3002', TRANSFERS_ENDPOINT: '0.0.0.0:3000', + SERVICES_ENDPOINT: '', BULK_TRANSFERS_ENDPOINT: '', THIRDPARTY_REQUESTS_ENDPOINT: '', TRANSACTION_REQUEST_ENDPOINT: '', diff --git a/src/shared/deferred-job.ts b/src/shared/deferred-job.ts new file mode 100644 index 00000000..0076607a --- /dev/null +++ b/src/shared/deferred-job.ts @@ -0,0 +1,206 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +/** + * deferredJob is a workflow to + * - setup pub/sub one time subscription to channel + * - initiate the workflow start by jobInitiator callback + * - consume published message by jobListener callback + * - wait for workflow to fulfill till timeout reached + */ + +import { timeout as prTimeout } from 'promise-timeout' +import { PubSub, Message } from '~/shared/pub-sub' + +// re-export TimeoutError so client will not be bothered about promise-timeout +export { TimeoutError } from 'promise-timeout' + +// function responsible for initiate the flow which should result, somewhere in the future, +// in publishing message to the queue +// parameter to deferredJob(...).init(jobInitiator) +export type JobInitiator = (channel: string, sid: number) => Promise; + +// function responsible for consuming the message +// parameter to deferredJob(...).init().job(jobListener) +export type JobListener = (message: Message) => Promise; + +// minimal mvp validation for JobInitiator +export class InitiatorRequired extends Error { + public channel: string + + constructor (channel: string) { + super(`'init' expects JobInitiator value for channel: '${channel}'`) + this.channel = channel + } + + // validation logic + static throwIfInvalid (channel: string, jobInitiator: JobInitiator): void { + if (typeof jobInitiator !== 'function') { + throw new InitiatorRequired(channel) + } + } +} + +// minimal mvp validation for JobListener +export class ListenerRequired extends Error { + public channel: string + + constructor (channel: string) { + super(`'job' expects JobListener value for channel: '${channel}'`) + this.channel = channel + } + + // validation logic + static throwIfInvalid (channel: string, jobListener: JobListener): void { + if (typeof jobListener !== 'function') { + throw new ListenerRequired(channel) + } + } +} + +// minimal mvp validation for timeout +export class PositiveTimeoutRequired extends Error { + public channel: string + + constructor (channel: string) { + super(`'wait' expects to be positive number for channel: '${channel}'`) + this.channel = channel + } + + // validation logic + static throwIfInvalid (channel: string, timeout: number): void { + if (timeout <= 0) { + throw new PositiveTimeoutRequired(channel) + } + } +} + +// async method which returns promise resolved when JobListener consume the Message +// this method invokes JobInitiator and setup promise timeout +// throws TimeoutError if Message isn't published or JobListener doesn't finish Message consumption in time +// https://www.npmjs.com/package/promise-timeout +export interface DeferredWait { + wait: (timeout: number) => Promise +} + +// method to setup JobListener +// returns interface with next possible step method - DeferredWait +export interface DeferredJob { + job: (jobListener: JobListener) => DeferredWait +} + +// only two methods are allowed on fresh result from deferredJob function +// these two methods reflects two possible flows +// - init method -> setups JobInitiator and returns interface to setupDeferredJob +// which will effects in DeferredWait interface at end +// - trigger method -> used to publish message to the channel + +export interface DeferredInitOrTrigger { + init: (jobInitiator: JobInitiator) => DeferredJob + trigger: (message: Message) => Promise +} + +// deferredJob +export default function deferredJob (cache: PubSub, channel: string): DeferredInitOrTrigger { + return { + + // initialize the deferred job + init: (jobInitiator: JobInitiator) => { + // mvp validation for jobInitiator + InitiatorRequired.throwIfInvalid(channel, jobInitiator) + return { + job: (jobListener: JobListener) => { + // mvp validation for jobListener + ListenerRequired.throwIfInvalid(channel, jobListener) + return { + wait: async (timeout = 2000): Promise => { + // mvp validation for timeout + PositiveTimeoutRequired.throwIfInvalid(channel, timeout) + + // cache subscription id + let sid = 0 + // cache un-subscription wrapper + const unsubscribe = (): void => { + // unsubscribe only if elements needed are valid + if (sid && cache && channel) { + cache.unsubscribe(channel, sid) + // protect against multiple un-subscription + sid = 0 + } + } + + // eslint-disable-next-line no-async-promise-executor + const promise = new Promise(async (resolve, reject) => { + try { + // subscribe to the channel to execute the jobListener when the message arrive + sid = await cache.subscribe(channel, async (_channel, message: Message) => { + // consume message + try { + // unsubscribe first to be sure the jobListener will be executed only once + // and system resources are preserved + await unsubscribe() + + // invoke deferred job to consume received message + await jobListener(message) + } catch (err) { + return reject(err) + } + + // done + resolve() + }) + + // invoke the async task which should effects in the future + // by publishing the message to channel via trigger method + // so the jobListener will be invoked + await jobInitiator(channel, sid) + } catch (err) { + // unsubscribe from channel in case of any error + await unsubscribe() + reject(err) + } + }) + + // ensure the whole process will finish in specified timeout + // throws error if timeout happens + return prTimeout(promise, timeout) + .catch(async (err) => { + await unsubscribe() + throw err + }) + } + } + } + } + }, + + // trigger the deferred job + trigger: async (message: Message): Promise => { + return cache.publish(channel, message) + } + } +} diff --git a/src/shared/http-scheme.ts b/src/shared/http-scheme.ts new file mode 100644 index 00000000..f61ce07a --- /dev/null +++ b/src/shared/http-scheme.ts @@ -0,0 +1,45 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +// HTTP/S scheme allowed variations +export enum Scheme { + http = 'http', + https = 'https' +} + +export type PrependFun = (absoluteURI: string) => string + +// URI building HTTP scheme related utilities +export const prepend2Uri = (scheme: Scheme) => (absoluteURI: string): string => `${scheme}://${absoluteURI}` +export const prependHttp2Uri = prepend2Uri(Scheme.http) +export const prependHttps2Uri = prepend2Uri(Scheme.https) + +export default { + prepend2Uri, + prependHttp2Uri, + prependHttps2Uri +} diff --git a/src/shared/kvs.ts b/src/shared/kvs.ts new file mode 100644 index 00000000..25cfa777 --- /dev/null +++ b/src/shared/kvs.ts @@ -0,0 +1,94 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import { RedisConnection } from './redis-connection' +import { promisify } from 'util' + +export class InvalidKeyError extends Error { + constructor () { + super('key should be non empty string') + } + + static throwIfInvalid (key: string): void { + if (!(key?.length > 0)) { + throw new InvalidKeyError() + } + } +} + +// KVS class deliver simple key - value storage backed by Redis +export class KVS extends RedisConnection { + // retrieve the value for given key + // if there is no value for given key 'undefined' is returned + async get (key: string): Promise { + InvalidKeyError.throwIfInvalid(key) + + const asyncGet = promisify(this.client.get) + const value: string | null | undefined = await asyncGet.call(this.client, key) + + return typeof value === 'string' ? JSON.parse(value) : undefined + } + + // store the value for given key + async set (key: string, value: T): Promise { + InvalidKeyError.throwIfInvalid(key) + + const asyncSet = promisify(this.client.set) + const stringified = JSON.stringify(value) + + return asyncSet.call(this.client, key, stringified) as Promise + } + + // removes the value for given key + // TODO: investigate why this was working without a callback in + // `thirdparty-scheme-adapter` + async del (key: string): Promise { + InvalidKeyError.throwIfInvalid(key) + return new Promise((resolve, reject) => { + return this.client.del(key, function (err, response) { + if (err) { + return reject(err) + } + return resolve(response > 0) + }) + }) + } + + // check is any data for given key + async exists (key: string): Promise { + // there is problem with TS typings + // so using `promisify` isn't working + return new Promise((resolve, reject) => { + this.client.exists(key, (err: unknown, result: number) => { + if (err) { + return reject(err) + } + resolve(result === 1) + }) + }) + } +} diff --git a/src/shared/pub-sub.ts b/src/shared/pub-sub.ts new file mode 100644 index 00000000..a174bbc0 --- /dev/null +++ b/src/shared/pub-sub.ts @@ -0,0 +1,182 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + - Lewis Daly + - Kevin Leyow + -------------- + ******/ + +import { RedisConnection } from './redis-connection' +import { promisify } from 'util' + +export class InvalidCallbackIdError extends Error { + constructor () { + super('valid callbackId must be positive number') + } + + // validation logic for callbackId parameter + static throwIfInvalid (callbackId: number): void { + if (!(callbackId > 0)) { + throw new InvalidCallbackIdError() + } + } +} + +export class InvalidChannelNameError extends Error { + constructor () { + super('channel name must be non empty string') + } + + // validation logic for channel name parameter + static throwIfInvalid (channel: string): void { + if (!(channel?.length)) { + throw new InvalidChannelNameError() + } + } +} + +// Message is send via redis publish to NotificationCallbacks +// Message should fully survive the JSON stringify/parse cycle +export type Message = string | number | boolean | Record + +export class InvalidMessageError extends Error { + public channel: string + + constructor (channel: string) { + super(`message received on channel: '${channel}' is invalid`) + this.channel = channel + } + + static throwIfInvalid (message: Message, channel: string): void { + if (typeof message === 'undefined' || (typeof message === 'object' && !message)) { + throw new InvalidMessageError(channel) + } + } +} +// NotificationCallback handles the Message +export type NotificationCallback = (channel: string, message: Message, id: number) => void + +// PubSub class delivers Message broadcast via Redis PUB/SUB mechanism to registered NotificationHandlers +export class PubSub extends RedisConnection { + // map where channels with registered notification callbacks are kept + private callbacks = new Map>() + + // counter used to generate callback registration id + private callbackId = 0 + + // overload RedisConnection.connect to add listener on messages + async connect (): Promise { + await super.connect() + this.client.on('message', this.broadcastMessage.bind(this)) + } + + // realize message broadcast over the channel to all registered notification callbacks + protected broadcastMessage (channel: string, stringified: string): void { + const callbacksForChannel = this.callbacks.get(channel) + + // do nothing if channel doesn't exist + if (!callbacksForChannel) { + this.logger.info(`broadcastMessage: no callbacks for '${channel}' channel`) + return + } + + // deserialize Message + // it is 'publish' duty to always send to channel well serialized Messages + const message: Message = JSON.parse(stringified) + + // do the validation of received Message + InvalidMessageError.throwIfInvalid(message, channel) + + // broadcast message by calling all callbacks + callbacksForChannel.forEach( + (callback: NotificationCallback, id: number): void => callback(channel, message, id) + ) + } + + // generate next callback id to be used as reference for unregister + protected get nextCallbackId (): number { + return ++this.callbackId + } + + // subscribe notification callback to message channel + subscribe (channel: string, callback: NotificationCallback): number { + InvalidChannelNameError.throwIfInvalid(channel) + + // is callbacks map for channel present + if (!this.callbacks.has(channel)) { + // initialize the channel callbacks map + this.callbacks.set(channel, new Map()) + + // only once time subscribe to Redis channel + this.client.subscribe(channel) + } + + const callbacksForChannel = this.callbacks.get(channel) + + // allocate new id for callback + const id = this.nextCallbackId + + // register callback for given channel + callbacksForChannel?.set(id, callback) + + // return registration id to be used by unsubscribe method + return id + } + + // unsubscribe from channel the notification callback for given callbackId reference + unsubscribe (channel: string, callbackId: number): boolean { + // input parameters validation + InvalidChannelNameError.throwIfInvalid(channel) + InvalidCallbackIdError.throwIfInvalid(callbackId) + + // do nothing if there is no channel + const callbacksForChannel = this.callbacks.get(channel) + if (!callbacksForChannel) { + return false + } + + // do nothing if there is no callback for registration id + if (!callbacksForChannel.has(callbackId)) { + return false + } + + // unregister callback + callbacksForChannel.delete(callbackId) + return true + } + + // publish a message to the given channel + // Message should fully survive the JSON stringify/parse cycle + async publish (channel: string, message: Message): Promise { + InvalidChannelNameError.throwIfInvalid(channel) + + // protect against publishing invalid Messages + InvalidMessageError.throwIfInvalid(message, channel) + + // serialized Messages should be properly deserialized by broadcastMessage + const stringified = JSON.stringify(message) + const asyncPublish = promisify(this.client.publish) + await asyncPublish.call(this.client, channel, stringified) + } +} diff --git a/src/shared/redis-connection.ts b/src/shared/redis-connection.ts new file mode 100644 index 00000000..5eeaab38 --- /dev/null +++ b/src/shared/redis-connection.ts @@ -0,0 +1,215 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import { RedisClient, createClient } from 'redis' +import { promisify } from 'util' +import { Logger as SDKLogger } from '@mojaloop/sdk-standard-components' + +export class RedisConnectionError extends Error { + public readonly host: string + public readonly port: number + + constructor (port: number, host: string) { + super(`can not connect to ${host}:${port}`) + this.host = host + this.port = port + } +} + +export class InvalidPortError extends Error { + constructor () { + super('port should be non negative number') + } + + static throwIfInvalid (port: number): void { + if (!(port > 0)) { + throw new InvalidPortError() + } + } +} + +export class InvalidLoggerError extends Error { + constructor () { + super('logger should be valid') + } + + static throwIfInvalid (logger: SDKLogger.Logger): void { + if (!(logger)) { + throw new InvalidLoggerError() + } + } +} + +export class InvalidHostError extends Error { + constructor () { + super('host should be non empty string') + } + + static throwIfInvalid (host: string): void { + if (!(host?.length > 0)) { + throw new InvalidHostError() + } + } +} + +export interface RedisConnectionConfig { + host: string; + port: number; + logger: SDKLogger.Logger; + timeout?: number +} + +export class RedisConnection { + protected readonly config: RedisConnectionConfig + + private redisClient: RedisClient = null as unknown as RedisClient + + static readonly defaultTimeout = 3000 + + constructor (config: RedisConnectionConfig) { + // input validation + InvalidHostError.throwIfInvalid(config.host) + InvalidPortError.throwIfInvalid(config.port) + InvalidLoggerError.throwIfInvalid(config.logger) + + // keep a flat copy of config with default timeout + this.config = { timeout: RedisConnection.defaultTimeout, ...config } + } + + get client (): RedisClient { + // protect against any attempt to work with not connected redis client + if (!this.isConnected) { + throw new RedisConnectionError(this.port, this.host) + } + return this.redisClient + } + + get host (): string { + return this.config.host + } + + get port (): number { + return this.config.port + } + + get logger (): SDKLogger.Logger { + return this.config.logger + } + + get timeout (): number { + return this.config.timeout || RedisConnection.defaultTimeout + } + + get isConnected (): boolean { + return this.redisClient && this.redisClient.connected + } + + async connect (): Promise { + // do nothing if already connected + if (this.isConnected) { + return + } + + // connect to redis + this.redisClient = await this.createClient() + } + + async disconnect (): Promise { + // do nothing if already disconnected + if (!(this.isConnected)) { + return + } + + // disconnect from redis + const asyncQuit = promisify(this.client.quit) + await asyncQuit.call(this.client) + + // cleanup + this.redisClient = null as unknown as RedisClient + } + + async ping (): Promise { + const asyncPing = promisify(this.client.ping) + const response: string = await asyncPing.call(this.client) as string + return response === 'PONG' + } + + private async createClient (): Promise { + return new Promise((resolve, reject) => { + // the newly created redis client + const client = createClient(this.port, this.host) + + // flags to protect against multiple reject/resolve + let rejectCalled = false + let resolveCalled = false + let timeoutStarted = false + + // let listen on ready message and resolve promise only one time + client.on('ready', (): void => { + // do nothing if promise already resolved or rejected + if (rejectCalled || resolveCalled) { + return + } + + this.logger.info(`createClient: Connected to REDIS at: ${this.host}:${this.port}`) + + // remember we resolve the promise + resolveCalled = true + + // do resolve + resolve(client) + }) + + // let listen on all redis errors and log them + client.on('error', (err): void => { + this.logger.push({ err }) + this.logger.error('createClient: Error from REDIS client') + + // do nothing if promise is already resolved or rejected + if (resolveCalled || timeoutStarted || rejectCalled) { + return + } + + timeoutStarted = true + // give a chance to reconnect in `this.timeout` milliseconds + setTimeout(() => { + // reconnection was success + if (resolveCalled || rejectCalled) { + return + } + + // if we can't connect let quit - reconnection was a failure + client.quit(() => null) + + // remember that we reject the promise + rejectCalled = true + reject(err) + }, this.timeout) + }) + }) + } +} diff --git a/test/data/data.ts b/test/data/data.ts index ee5bce73..1e52aaed 100644 --- a/test/data/data.ts +++ b/test/data/data.ts @@ -1,6 +1,6 @@ import { Consent, ConsentCredential } from '~/model/consent' import { Request, ResponseToolkit, ResponseObject } from '@hapi/hapi' -import { Scope } from '~/model/scope' +import { ModelScope } from '~/model/scope' import { CredentialStatusEnum } from '~/model/consent/consent' import { thirdparty as tpAPI } from '@mojaloop/api-snippets'; @@ -174,7 +174,7 @@ export const externalScopes: tpAPI.Schemas.Scope[] = [{ } ] -export const scopes: Scope[] = [{ +export const scopes: ModelScope[] = [{ id: 123234, consentId: 'b51ec534-ee48-4575-b6a9-ead2955b8069', accountId: 'as2342', diff --git a/test/integration/domain/consents/ID.test.ts b/test/integration/domain/consents/ID.test.ts index a5652d90..12f51166 100644 --- a/test/integration/domain/consents/ID.test.ts +++ b/test/integration/domain/consents/ID.test.ts @@ -205,7 +205,7 @@ describe('server/domain/consents/{ID}', (): void => { attestationObject: consents[1].attestationObject! } - const mockconvertDatabaseScopesToThirdpartyScopes = jest.spyOn(Scopes, 'convertDatabaseScopesToThirdpartyScopes') + const mockconvertModelScopesToThirdpartyScopes = jest.spyOn(Scopes, 'convertModelScopesToThirdpartyScopes') const externalScopes: tpAPI.Schemas.Scope[] = [ { accountId: 'as2342', @@ -216,7 +216,7 @@ describe('server/domain/consents/{ID}', (): void => { actions: ['accounts.getBalance'] } ] - mockconvertDatabaseScopesToThirdpartyScopes.mockReturnValueOnce(externalScopes) + mockconvertModelScopesToThirdpartyScopes.mockReturnValueOnce(externalScopes) // Outgoing consent request body used for validation @@ -227,7 +227,7 @@ describe('server/domain/consents/{ID}', (): void => { status: 'VERIFIED', payload: { id: consents[1].credentialId!, - rawId: 'TODO: figure out what goes here', + rawId: consents[1].credentialId!, response: { // clientDataJSON needs to be utf-8 not base64 clientDataJSON: Buffer.from( diff --git a/test/integration/model/consent.test.ts b/test/integration/model/consent.test.ts index 158a8a18..0ebe3789 100644 --- a/test/integration/model/consent.test.ts +++ b/test/integration/model/consent.test.ts @@ -30,7 +30,7 @@ import Knex from 'knex' import Config from '~/shared/config' import { ConsentDB, Consent } from '../../../src/model/consent' -import { Scope } from '../../../src/model/scope' +import { ModelScope } from '../../../src/model/scope' import { NotFoundError } from '../../../src/model/errors' /* @@ -360,9 +360,9 @@ describe('src/model/consent', (): void => { await Db('Consent').insert(completeConsent) // Insert associated scopes - await Db('Scope').insert(tempScopes) + await Db('Scope').insert(tempScopes) - let scopes = await Db('Scope') + let scopes = await Db('Scope') .select('*') .where({ consentId: completeConsent.id @@ -374,7 +374,7 @@ describe('src/model/consent', (): void => { expect(deleteCount).toEqual(1) - scopes = await Db('Scope') + scopes = await Db('Scope') .select('*') .where({ consentId: completeConsent.id diff --git a/test/integration/model/scope.test.ts b/test/integration/model/scope.test.ts index 8fef8c8d..03c6e101 100644 --- a/test/integration/model/scope.test.ts +++ b/test/integration/model/scope.test.ts @@ -29,7 +29,7 @@ import Knex from 'knex' import Config from '~/shared/config' -import { ScopeDB, Scope } from '../../../src/model/scope' +import { ScopeDB, ModelScope } from '../../../src/model/scope' import { Consent } from '../../../src/model/consent' import { NotFoundError } from '../../../src/model/errors' @@ -54,7 +54,7 @@ const partialConsents: Consent[] = [ /* * Mock Scope Resources */ -const tempScopes: Scope[] = [ +const tempScopes: ModelScope[] = [ { consentId: partialConsents[0].id, action: 'transfer', @@ -92,7 +92,7 @@ describe('src/model/scope', (): void => { // Reset table for new test beforeEach(async (): Promise => { await Db('Consent').del() - await Db('Scope').del() + await Db('Scope').del() await Db('Consent').insert(partialConsents) }) @@ -104,7 +104,7 @@ describe('src/model/scope', (): void => { // Return type check expect(inserted).toEqual(true) - const scopes: Scope[] = await Db('Scope') + const scopes: ModelScope[] = await Db('Scope') .select('*') .where({ consentId: partialConsents[0].id @@ -123,7 +123,7 @@ describe('src/model/scope', (): void => { // Return type check expect(inserted).toEqual(true) - const scopes: Scope[] = await Db('Scope') + const scopes: ModelScope[] = await Db('Scope') .select('*') .where({ consentId: partialConsents[0].id @@ -148,11 +148,11 @@ describe('src/model/scope', (): void => { it('returns without affecting the DB on inserting empty scopes array', async (): Promise => { - const scopesInitial: Scope[] = await Db('Scope').select('*') + const scopesInitial: ModelScope[] = await Db('Scope').select('*') const inserted: boolean = await scopeDB.insert([]) - const scopesAfter: Scope[] = await Db('Scope').select('*') + const scopesAfter: ModelScope[] = await Db('Scope').select('*') expect(inserted).toEqual(true) // No effect on the DB @@ -164,9 +164,9 @@ describe('src/model/scope', (): void => { describe('retrieveAll', (): void => { it('retrieves only existing scopes from the database', async (): Promise => { - await Db('Scope').insert(tempScopes) + await Db('Scope').insert(tempScopes) - const scopes: Scope[] = await scopeDB.retrieveAll( + const scopes: ModelScope[] = await scopeDB.retrieveAll( tempScopes[0].consentId ) diff --git a/test/integration/redis/kvs.test.ts b/test/integration/redis/kvs.test.ts new file mode 100644 index 00000000..db51d548 --- /dev/null +++ b/test/integration/redis/kvs.test.ts @@ -0,0 +1,100 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import { KVS } from '~/shared/kvs' +import { RedisConnectionConfig } from '~/shared/redis-connection' +import Config from '~/shared/config' +import mockLogger from '../../unit/mockLogger' + +describe('KVS', () => { + const config: RedisConnectionConfig = { + host: Config.REDIS.HOST, + port: Config.REDIS.PORT, + logger: mockLogger(), + timeout: Config.REDIS.TIMEOUT + } + let kvs: KVS + + beforeAll(async (): Promise => { + kvs = new KVS(config) + await kvs.connect() + }) + + afterAll(async (): Promise => { + await kvs.disconnect() + }) + + it('should be connected', async (): Promise => { + expect(kvs.isConnected).toBeTruthy() + const result = await kvs.ping() + expect(result).toEqual(true) + }) + + it('should GET what was SET', async (): Promise => { + const values = [ + { a: 1, b: true, c: 'C', d: {} }, + true, + 123, + 'the-string' + ] + + for (const value of values) { + await kvs.set('key', value) + expect(await kvs.exists('key')).toBeTruthy() + const retrieved = await kvs.get('key') + expect(retrieved).toEqual(value) + } + }) + + it('GET should give \'unknown\' for not stored value', async (): Promise => { + const value = await kvs.get('key-for-never-stored-value') + expect(value).toBeUndefined() + }) + + it('should DEL was SET and next GET after should give unknown', async (): Promise => { + const values = [ + { a: 1, b: true, c: 'C', d: {} }, + true, + 123, + 'the-string' + ] + + for (const value of values) { + await kvs.set('key-del', value) + const retrieved = await kvs.get('key-del') + expect(retrieved).toEqual(value) + expect(await kvs.exists('key-del')).toBeTruthy() + const result = await kvs.del('key-del') + expect(result).toBeTruthy() + expect(await kvs.exists('key-del')).toBeFalsy() + const deleted = await kvs.get('key-del') + expect(deleted).toBeUndefined() + const resultDeleted = await kvs.del('key-del') + expect(resultDeleted).toBeFalsy() + } + }) +}) diff --git a/test/integration/redis/pub-sub.test.ts b/test/integration/redis/pub-sub.test.ts new file mode 100644 index 00000000..864296a4 --- /dev/null +++ b/test/integration/redis/pub-sub.test.ts @@ -0,0 +1,77 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import { Message, PubSub } from '~/shared/pub-sub' +import { RedisConnectionConfig } from '~/shared/redis-connection' +import { logger } from '~/shared/logger' +import Config from '~/shared/config' + +describe('PubSub', () => { + const config: RedisConnectionConfig = { + host: Config.REDIS.HOST, + port: Config.REDIS.PORT, + logger: logger, + timeout: Config.REDIS.TIMEOUT + } + let listener: PubSub + let publisher: PubSub + beforeAll(async (): Promise => { + listener = new PubSub(config) + await listener.connect() + publisher = new PubSub(config) + await publisher.connect() + }) + + afterAll(async (): Promise => { + await listener.disconnect() + await publisher.disconnect() + }) + + it('should be connected', async (): Promise => { + expect(listener.isConnected).toBeTruthy() + const result = await listener.ping() + expect(result).toEqual(true) + }) + + it('notification callback should received published message', (done): void => { + const msg: Message = { + a: 1, + b: 'B', + c: true, + d: { nested: true } + } + const messageHandler = (channel: string, message: Message, id: number) => { + expect(channel).toEqual('the-channel') + expect(id).toBe(cbId) + expect(message).toEqual(msg) + done() + } + + const cbId = listener.subscribe('the-channel', messageHandler) + publisher.publish('the-channel', msg) + }) +}) diff --git a/test/integration/redis/redis-connection.test.ts b/test/integration/redis/redis-connection.test.ts new file mode 100644 index 00000000..efba32b2 --- /dev/null +++ b/test/integration/redis/redis-connection.test.ts @@ -0,0 +1,65 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import { RedisConnection, RedisConnectionConfig } from '~/shared/redis-connection' +import mockLogger from '../../unit/mockLogger' +import Config from '~/shared/config' + +describe('RedisConnection', () => { + const config: RedisConnectionConfig = { + host: Config.REDIS.HOST, + port: Config.REDIS.PORT, + logger: mockLogger(), + timeout: Config.REDIS.TIMEOUT + } + let rc: RedisConnection + + beforeAll(async (): Promise => { + rc = new RedisConnection(config) + await rc.connect() + }) + + afterAll(async (): Promise => { + await rc.disconnect() + }) + + it('should be connected', async (): Promise => { + expect(rc.isConnected).toBeTruthy() + const result = await rc.ping() + expect(result).toEqual(true) + }) + + it('should throw error if can\'t connect', async (): Promise => { + const invalidPort = { ...config, timeout: 200 } + invalidPort.port = 8080 + const invalidRC = new RedisConnection(invalidPort) + expect(invalidRC.connect()).rejects.toThrowError( + new Error('Redis connection to localhost:8080 failed - connect ECONNREFUSED 127.0.0.1:8080') + ) + expect(invalidRC.isConnected).toBeFalsy() + }) +}) diff --git a/test/unit/__mocks__/redis.js b/test/unit/__mocks__/redis.js new file mode 100644 index 00000000..18e99473 --- /dev/null +++ b/test/unit/__mocks__/redis.js @@ -0,0 +1,58 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - James Bush - james.bush@modusbox.com + -------------- + ******/ + +const redisMock = require('redis-mock') + +// redis-mock currently ignores callback argument, the following class fix this +class RedisClient extends redisMock.RedisClient { + _executeCallback (...args) { + if (typeof args[args.length - 1] === 'function') { + const callback = args[args.length - 1] + const argList = Array.prototype.slice.call(args, 0, args.length - 1) + callback(null, argList) + } + } + + subscribe (...args) { + super.subscribe(...args) + this._executeCallback(...args) + } + + publish (...args) { + super.publish(...args) + this._executeCallback(...args) + } + + set (...args) { + super.set(...args) + this._executeCallback(...args) + } +} + +module.exports = { + createClient: () => new RedisClient() +} diff --git a/test/unit/domain/authorizations.test.ts b/test/unit/domain/authorizations.test.ts index ce7b163e..d5df365b 100644 --- a/test/unit/domain/authorizations.test.ts +++ b/test/unit/domain/authorizations.test.ts @@ -30,7 +30,7 @@ import { Request } from '@hapi/hapi' import { Enum } from '@mojaloop/central-services-shared' // import { logger } from '~/shared/logger' import { Consent } from '~/model/consent' -import { Scope } from '~/model/scope' +import { ModelScope } from '~/model/scope' import { thirdPartyRequest } from '~/domain/requests' import { AuthPayload, @@ -158,7 +158,7 @@ describe('Incoming POST Transaction Authorization Domain', (): void => { value: 'dwuduwd&e2idjoj0w' } - const consentScopes: Scope[] = [ + const consentScopes: ModelScope[] = [ { id: 1, consentId: payload.consentId, @@ -195,7 +195,7 @@ describe('Incoming POST Transaction Authorization Domain', (): void => { value: 'dwuduwd&e2idjoj0w' } - const consentScopes: Scope[] = [ + const consentScopes: ModelScope[] = [ { id: 1, consentId: payload.consentId, @@ -232,7 +232,7 @@ describe('Incoming POST Transaction Authorization Domain', (): void => { value: 'dwuduwd&e2idjoj0w' } - const consentScopes: Scope[] = [] + const consentScopes: ModelScope[] = [] const scopeMatch = hasMatchingScopeForPayload(consentScopes, payload) diff --git a/test/unit/domain/consents/ID.test.ts b/test/unit/domain/consents/ID.test.ts index 88d0ad5a..c7cc4bca 100644 --- a/test/unit/domain/consents/ID.test.ts +++ b/test/unit/domain/consents/ID.test.ts @@ -43,14 +43,14 @@ import { } from '~/domain/errors' import { CredentialStatusEnum, ConsentCredential } from '~/model/consent/consent' import { UpdateCredentialRequest } from '~/domain/consents' -import { Scope } from '~/model/scope' +import { ModelScope } from '~/model/scope' import { thirdparty as tpAPI } from '@mojaloop/api-snippets'; const mockConsentDbRetrieve = jest.spyOn(consentDB, 'retrieve') const mockConsentDbUpdate = jest.spyOn(consentDB, 'update') const mockScopeDbRetrieveAll = jest.spyOn(scopeDB, 'retrieveAll') const mockPutConsentsOutbound = jest.spyOn(thirdPartyRequest, 'putConsents') -const mockconvertDatabaseScopesToThirdpartyScopes = jest.spyOn(Scopes, 'convertDatabaseScopesToThirdpartyScopes') +const mockconvertModelScopesToThirdpartyScopes = jest.spyOn(Scopes, 'convertModelScopesToThirdpartyScopes') const requestClientDataJSON: string = Buffer.from( @@ -275,7 +275,7 @@ const requestBody: tpAPI.Schemas.ConsentsIDPutResponseVerified = { status: CredentialStatusEnum.VERIFIED, payload: { id: requestCredentialId, - rawId: 'TODO: figure out what goes here', + rawId: requestCredentialId, response: { clientDataJSON: requestClientDataJSON, attestationObject: requestAttestationObject @@ -290,7 +290,7 @@ describe('server/domain/consents/{ID}', (): void => { mockConsentDbRetrieve.mockResolvedValue(retrievedConsent) mockScopeDbRetrieveAll.mockResolvedValue(retrievedScopes) mockPutConsentsOutbound.mockResolvedValue(undefined) - mockconvertDatabaseScopesToThirdpartyScopes.mockReturnValue(externalScopes) + mockconvertModelScopesToThirdpartyScopes.mockReturnValue(externalScopes) mockConsentDbUpdate.mockResolvedValue(2) }) @@ -398,7 +398,7 @@ describe('server/domain/consents/{ID}', (): void => { expect(returnedBody).toStrictEqual(requestBody) expect(mockScopeDbRetrieveAll).toBeCalledWith(consentId) - expect(mockconvertDatabaseScopesToThirdpartyScopes).toBeCalledWith(retrievedScopes) + expect(mockconvertModelScopesToThirdpartyScopes).toBeCalledWith(retrievedScopes) }) it('should promulgate scope retrieval error.', @@ -409,13 +409,13 @@ describe('server/domain/consents/{ID}', (): void => { .toThrowError('Test') expect(mockScopeDbRetrieveAll).toBeCalledWith(consentId) - expect(mockconvertDatabaseScopesToThirdpartyScopes).not.toBeCalled() + expect(mockconvertModelScopesToThirdpartyScopes).not.toBeCalled() }) it('should promulgate scope conversion error.', async (): Promise => { - mockconvertDatabaseScopesToThirdpartyScopes.mockImplementationOnce( - (_scopes: Scope[]): tpAPI.Schemas.Scope[] => { + mockconvertModelScopesToThirdpartyScopes.mockImplementationOnce( + (_scopes: ModelScope[]): tpAPI.Schemas.Scope[] => { throw new Error('Test') }) await expect(buildConsentsIDPutResponseVerifiedBody(retrievedConsent)) @@ -423,7 +423,7 @@ describe('server/domain/consents/{ID}', (): void => { .toThrowError('Test') expect(mockScopeDbRetrieveAll).toBeCalledWith(consentId) - expect(mockconvertDatabaseScopesToThirdpartyScopes).toBeCalledWith(retrievedScopes) + expect(mockconvertModelScopesToThirdpartyScopes).toBeCalledWith(retrievedScopes) }) }) }) diff --git a/test/unit/domain/scopes.test.ts b/test/unit/domain/scopes.test.ts index d9d683c6..ea4a2644 100644 --- a/test/unit/domain/scopes.test.ts +++ b/test/unit/domain/scopes.test.ts @@ -28,13 +28,13 @@ -------------- ******/ -import { Scope } from '~/model/scope/scope' +import { ModelScope } from '~/model/scope/scope' import * as ScopeFunctions from '~/domain/scopes' import { thirdparty as tpAPI } from '@mojaloop/api-snippets'; const consentId = '1234' -const scopes: Scope[] = [{ +const scopes: ModelScope[] = [{ id: 123234, consentId: '1234', accountId: 'as2342', @@ -54,7 +54,7 @@ const scopes: Scope[] = [{ } ] -const scopesNoId: Scope[] = [{ +const scopesNoId: ModelScope[] = [{ consentId: '1234', accountId: 'as2342', action: 'accounts.getBalance' @@ -83,7 +83,7 @@ const externalScope: tpAPI.Schemas.Scope[] = [{ describe('Scope Convert Scopes to ExternalScopes', (): void => { it('Should return Scope array when input ExternalScope array', (): void => { - expect(ScopeFunctions.convertDatabaseScopesToThirdpartyScopes(scopes)) + expect(ScopeFunctions.convertModelScopesToThirdpartyScopes(scopes)) .toStrictEqual(externalScope) }) }) diff --git a/test/unit/mockLogger.ts b/test/unit/mockLogger.ts new file mode 100644 index 00000000..c6860656 --- /dev/null +++ b/test/unit/mockLogger.ts @@ -0,0 +1,54 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import { Logger as SDKLogger } from '@mojaloop/sdk-standard-components' + +export default function mockLogger (keepQuiet = true): SDKLogger.Logger { + if (keepQuiet) { + const methods = { + // log methods + log: jest.fn(), + + configure: jest.fn(), + + // generated methods from default levels + verbose: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + trace: jest.fn(), + info: jest.fn(), + fatal: jest.fn() + } + return { + ...methods, + push: jest.fn(() => methods) + } as unknown as SDKLogger.Logger + } + // let be elaborative and log to console + return new SDKLogger.Logger() +} diff --git a/test/unit/model/persistent.model.test.ts b/test/unit/model/persistent.model.test.ts new file mode 100644 index 00000000..9650867e --- /dev/null +++ b/test/unit/model/persistent.model.test.ts @@ -0,0 +1,285 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import { KVS } from '~/shared/kvs' +import { RedisConnectionConfig } from '~/shared/redis-connection' +import { StateMachineConfig, Method } from 'javascript-state-machine' +import { + ControlledStateMachine, + PersistentModel, + PersistentModelConfig, + StateData, + create, + loadFromKVS +} from '~/model/persistent.model' +import { mocked } from 'ts-jest/utils' +import mockLogger from 'test/unit/mockLogger' +import shouldNotBeExecuted from 'test/unit/shouldNotBeExecuted' +import sortedArray from 'test/unit/sortedArray' + +// mock KVS default exported class +jest.mock('~/shared/kvs') + +describe('PersistentModel', () => { + interface TestStateMachine extends ControlledStateMachine { + start2End: Method; + start2Middle: Method; + middle2End: Method; + + onStart2End: Method; + onStart2Middle: Method; + onMiddle2End: Method; + } + + interface TestData extends StateData { + the: string; + } + + const KVSConfig: RedisConnectionConfig = { + port: 6789, + host: 'localhost', + logger: mockLogger() + } + let modelConfig: PersistentModelConfig + let smConfig: StateMachineConfig + + let data: TestData + + function checkPSMLayout (pm: PersistentModel, optData?: TestData) { + expect(pm).toBeTruthy() + expect(pm.fsm.state).toEqual(optData?.currentState || smConfig.init || 'none') + + expect(pm.data).toBeDefined() + expect(pm.data.currentState).toEqual(pm.fsm.state) + // test get accessors + expect(pm.kvs).toEqual(modelConfig.kvs) + expect(pm.key).toEqual(modelConfig.key) + expect(pm.logger).toEqual(modelConfig.logger) + + expect(typeof pm.onAfterTransition).toEqual('function') + expect(typeof pm.onPendingTransition).toEqual('function') + expect(typeof pm.saveToKVS).toEqual('function') + expect(typeof pm.fsm.init).toEqual('function') + expect(typeof pm.fsm.start2End).toEqual('function') + expect(typeof pm.fsm.start2Middle).toEqual('function') + expect(typeof pm.fsm.middle2End).toEqual('function') + expect(typeof pm.fsm.error).toEqual('function') + expect(sortedArray(pm.fsm.allStates())).toEqual(['end', 'errored', 'middle', 'none', 'start']) + expect(sortedArray(pm.fsm.allTransitions())).toEqual(['error', 'init', 'middle2End', 'start2End', 'start2Middle']) + } + + beforeEach(async () => { + smConfig = { + init: 'start', + transitions: [ + { name: 'start2End', from: 'start', to: 'end' }, + { name: 'start2Middle', from: 'start', to: 'middle' }, + { name: 'middle2End', from: 'middle', to: 'end' } + ], + methods: { + onStart2End: jest.fn(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, 50) + }) + }), + onStart2Middle: jest.fn(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, 100) + }) + }), + onMiddle2End: jest.fn(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, 100) + }) + }), + onError: jest.fn(() => { + console.error('onError') + }) + } + } + + // model config + modelConfig = { + kvs: new KVS(KVSConfig), + key: 'cache-key', + logger: KVSConfig.logger + } + // test data + data = { the: 'data' } as TestData + + await modelConfig.kvs.connect() + }) + + afterEach(async () => { + await modelConfig.kvs.disconnect() + }) + + it('module layout', () => { + expect(typeof PersistentModel).toEqual('function') + expect(typeof loadFromKVS).toEqual('function') + expect(typeof create).toEqual('function') + }) + + it('create with initial state not specified -> no auto transition', async () => { + const withoutInitialState = { ...smConfig } + delete withoutInitialState.init + const pm = await create(data, modelConfig, withoutInitialState) + checkPSMLayout(pm, { currentState: 'none' } as TestData) + }) + + it('create with initial state not specified -> with auto transition to start', async () => { + const withoutInitialStateAuto = { ...smConfig } + delete withoutInitialStateAuto.init + withoutInitialStateAuto.transitions.push({ name: 'init', from: 'none', to: 'start' }) + const pm = await create(data, modelConfig, withoutInitialStateAuto) + checkPSMLayout(pm, { currentState: 'start' } as TestData) + expect(KVSConfig.logger.info).toHaveBeenCalledWith('State machine transitioned \'init\': none -> start') + }) + + it('create always with auto transition to default state when auto transition defined', async () => { + const withInitialStateAutoOverload = { ...smConfig } + // we would like to set initial state to 'end' + withInitialStateAutoOverload.init = 'end' + + // and there is initial auto transition which should transist to start + withInitialStateAutoOverload.transitions.push({ name: 'init', from: 'none', to: 'start' }) + + const pm = await create( + data, modelConfig, withInitialStateAutoOverload + ) + + // check auto transition ended at 'start' not 'end' + checkPSMLayout(pm, { currentState: 'start' } as TestData) + expect(KVSConfig.logger.info).toHaveBeenCalledWith('State machine transitioned \'init\': none -> start') + }) + it('create with initial state \'end\' specified', async () => { + const endConfig = { ...smConfig } + endConfig.init = 'end' + const pm = await create(data, modelConfig, endConfig) + checkPSMLayout(pm, { currentState: 'end' } as TestData) + expect(KVSConfig.logger.info).toHaveBeenCalledWith('State machine transitioned \'init\': none -> end') + }) + + it('create with initial state \'start\' specified', async () => { + const pm = await create(data, modelConfig, smConfig) + checkPSMLayout(pm, { currentState: 'start' } as TestData) + }) + describe('transition notifications', () => { + it('should call notification handlers', async () => { + const pm = await create(data, modelConfig, smConfig) + checkPSMLayout(pm, { currentState: 'start' } as TestData) + + await pm.fsm.start2Middle() + expect(smConfig.methods!.onStart2Middle).toBeCalledTimes(1) + await pm.fsm.middle2End() + expect(smConfig.methods!.onMiddle2End).toBeCalledTimes(1) + }) + }) + describe('onPendingTransition', () => { + it('should throw error if not `error` transition', async () => { + const pm = new PersistentModel(data, modelConfig, smConfig) + checkPSMLayout(pm, { currentState: 'start' } as TestData) + expect( + () => pm.fsm.start2Middle() + ).toThrowError('Transition \'start2Middle\' requested while another transition is in progress') + }) + + it('should not throw error if `error` transition called when any transition is pending', async () => { + const pm = await create(data, modelConfig, smConfig) + checkPSMLayout(pm) + expect(pm.fsm.state).toBe('start') + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(pm.fsm._fsm.pending).toBe(false) + // not awaiting on start2Middle to resolve! + pm.fsm.start2Middle() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(pm.fsm._fsm.pending).toBe(true) + expect(() => pm.fsm.error()).not.toThrow() + }) + }) + + describe('loadFromKVS', () => { + it('should properly call `KVS.get`, get expected data in `context.data` and setup state of machine', async () => { + const dataFromCache: TestData = { the: 'data from cache', currentState: 'end' } + mocked(modelConfig.kvs.get).mockImplementationOnce(async () => dataFromCache) + const pm = await loadFromKVS(modelConfig, smConfig) + checkPSMLayout(pm, dataFromCache) + + // to get value from cache proper key should be used + expect(mocked(modelConfig.kvs.get)).toHaveBeenCalledWith(modelConfig.key) + + // check what has been stored in `data` + expect(pm.data).toEqual(dataFromCache) + }) + + it('should throw when received invalid data from `KVS.get`', async () => { + mocked(modelConfig.kvs.get).mockImplementationOnce(async () => null) + try { + await loadFromKVS(modelConfig, smConfig) + shouldNotBeExecuted() + } catch (error) { + expect(error.message).toEqual(`No data found in KVS for: ${modelConfig.key}`) + } + }) + + it('should propagate error received from `KVS.get`', async () => { + mocked(modelConfig.kvs.get).mockImplementationOnce(jest.fn(async () => { throw new Error('error from KVS.get') })) + expect(() => loadFromKVS(modelConfig, smConfig)) + .rejects.toEqual(new Error('error from KVS.get')) + }) + }) + describe('saveToKVS', () => { + it('should store using KVS.set', async () => { + mocked(modelConfig.kvs.set).mockImplementationOnce(() => { throw new Error('error from KVS.set') }) + + const pm = await create(data, modelConfig, smConfig) + checkPSMLayout(pm) + + // transition `init` should encounter exception when saving `context.data` + expect(() => pm.saveToKVS()).rejects.toEqual(new Error('error from KVS.set')) + expect(mocked(modelConfig.kvs.set)).toBeCalledWith(pm.key, pm.data) + }) + it('should propagate error from KVS.set', async () => { + mocked(modelConfig.kvs.set).mockImplementationOnce(() => { throw new Error('error from KVS.set') }) + + const pm = await create(data, modelConfig, smConfig) + checkPSMLayout(pm) + + // transition `init` should encounter exception when saving `context.data` + expect(() => pm.saveToKVS()).rejects.toEqual(new Error('error from KVS.set')) + expect(mocked(modelConfig.kvs.set)).toBeCalledWith(pm.key, pm.data) + }) + }) +}) diff --git a/test/unit/model/scope.test.ts b/test/unit/model/scope.test.ts index 6220d35e..905e0a55 100644 --- a/test/unit/model/scope.test.ts +++ b/test/unit/model/scope.test.ts @@ -29,7 +29,7 @@ import Knex from 'knex' import Config from '~/shared/config' -import { Scope, ScopeDB } from '~/model/scope' +import { ModelScope, ScopeDB } from '~/model/scope' import { Consent } from '~/model/consent' import { NotFoundError } from '~/model/errors' @@ -53,19 +53,19 @@ const partialConsent2: Consent = { /* * Mock Scope Resources */ -const tempScope1: Scope = { +const tempScope1: ModelScope = { consentId: partialConsent1.id, action: 'transfer', accountId: 'sjdn-3333-2123' } -const tempScope2: Scope = { +const tempScope2: ModelScope = { consentId: partialConsent1.id, action: 'balance', accountId: 'sjdn-q333-2123' } -const tempScope3: Scope = { +const tempScope3: ModelScope = { consentId: partialConsent1.id, action: 'saving', accountId: 'sjdn-q333-2123' @@ -94,7 +94,7 @@ describe('src/model/scope', (): void => { // Reset table for new test beforeEach(async (): Promise => { await Db('Consent').del() - await Db('Scope').del() + await Db('Scope').del() await Db('Consent') .insert([partialConsent1, partialConsent2]) }) @@ -107,7 +107,7 @@ describe('src/model/scope', (): void => { expect(inserted).toEqual(true) // Assertion - const scopes: Scope[] = await Db('Scope') + const scopes: ModelScope[] = await Db('Scope') .select('*') .where({ consentId: partialConsent1.id @@ -125,7 +125,7 @@ describe('src/model/scope', (): void => { expect(inserted).toEqual(true) // Assertion - const scopes: Scope[] = await Db('Scope') + const scopes: ModelScope[] = await Db('Scope') .select('*') .where({ consentId: partialConsent1.id @@ -147,12 +147,12 @@ describe('src/model/scope', (): void => { it('returns without affecting the DB on inserting empty scopes array', async (): Promise => { // Assertion - const scopesInitial: Scope[] = await Db('Scope') + const scopesInitial: ModelScope[] = await Db('Scope') .select('*') const inserted: boolean = await scopeDB.insert([]) - const scopesAfter: Scope[] = await Db('Scope') + const scopesAfter: ModelScope[] = await Db('Scope') .select('*') expect(inserted).toEqual(true) @@ -164,11 +164,11 @@ describe('src/model/scope', (): void => { describe('retrieveAll', (): void => { it('retrieves only existing scopes from the database', async (): Promise => { // Setup - await Db('Scope') + await Db('Scope') .insert([tempScope1, tempScope2, tempScope3]) // Action - const scopes: Scope[] = await scopeDB.retrieveAll(tempScope1.consentId) + const scopes: ModelScope[] = await scopeDB.retrieveAll(tempScope1.consentId) // Assertion expect(scopes.length).toEqual(3) diff --git a/test/unit/server/plugins/state.test.ts b/test/unit/server/plugins/state.test.ts new file mode 100644 index 00000000..aa8e2ab5 --- /dev/null +++ b/test/unit/server/plugins/state.test.ts @@ -0,0 +1,91 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + + -------------- + ******/ + +import { KVS } from '~/shared/kvs' +import { PubSub } from '~/shared/pub-sub' +import { StatePlugin } from '~/server/plugins/state' +import { mocked } from 'ts-jest/utils' +import { Server } from '@hapi/hapi' +import { mockProcessExit } from 'jest-mock-process' + +jest.mock('~/shared/kvs') +jest.mock('~/shared/pub-sub') +jest.mock('~/shared/logger') + +describe('StatePlugin', () => { + const ServerMock = { + events: { + on: jest.fn() + }, + decorate: jest.fn() + } + + it('should have proper layout', () => { + expect(typeof StatePlugin.name).toEqual('string') + expect(typeof StatePlugin.version).toEqual('string') + expect(StatePlugin.once).toBeTruthy() + expect(StatePlugin.register).toBeInstanceOf(Function) + }) + + it('happy flow: should properly register', async () => { + mocked(KVS.prototype.connect).mockImplementationOnce(() => Promise.resolve()) + mocked(PubSub.prototype.connect).mockImplementation(() => Promise.resolve()) + + await StatePlugin.register(ServerMock as unknown as Server) + + // check decoration + expect(ServerMock.decorate) + expect(ServerMock.decorate.mock.calls[0][0]).toEqual('toolkit') + expect(ServerMock.decorate.mock.calls[0][1]).toEqual('getKVS') + expect(ServerMock.decorate.mock.calls[1][0]).toEqual('toolkit') + expect(ServerMock.decorate.mock.calls[1][1]).toEqual('getPublisher') + expect(ServerMock.decorate.mock.calls[2][0]).toEqual('toolkit') + expect(ServerMock.decorate.mock.calls[2][1]).toEqual('getSubscriber') + expect(ServerMock.decorate.mock.calls[3][0]).toEqual('toolkit') + expect(ServerMock.decorate.mock.calls[3][1]).toEqual('getLogger') + expect(ServerMock.decorate.mock.calls[4][0]).toEqual('toolkit') + expect(ServerMock.decorate.mock.calls[4][1]).toEqual('getMojaloopRequests') + expect(ServerMock.decorate.mock.calls[5][0]).toEqual('toolkit') + expect(ServerMock.decorate.mock.calls[5][1]).toEqual('getThirdpartyRequests') + + // check listener registration on 'stop' event + expect(ServerMock.events.on).toBeCalledTimes(1) + expect(ServerMock.events.on.mock.calls[0][0]).toEqual('stop') + }) + + it('exceptions: should properly register', async () => { + // eslint-disable-next-line prefer-promise-reject-errors + mocked(KVS.prototype.connect).mockImplementationOnce(() => Promise.reject('can not connect')) + mocked(PubSub.prototype.connect).mockImplementation(() => Promise.resolve()) + + const mockExit = mockProcessExit() + await StatePlugin.register(ServerMock as unknown as Server) + expect(mockExit).toBeCalledWith(1) + mockExit.mockRestore() + }) +}) diff --git a/test/unit/shared/deferred-job.test.ts b/test/unit/shared/deferred-job.test.ts new file mode 100644 index 00000000..6af8aa53 --- /dev/null +++ b/test/unit/shared/deferred-job.test.ts @@ -0,0 +1,180 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import deferredJob, { JobInitiator, JobListener, PositiveTimeoutRequired, TimeoutError } from '~/shared/deferred-job' +import mockLogger from '../mockLogger' +import { Message, NotificationCallback, PubSub } from '~/shared/pub-sub' +import { + RedisConnectionConfig +} from '~/shared/redis-connection' +import { v4 as uuidv4 } from 'uuid' +jest.mock('redis') + +describe('deferredJob', () => { + test('module layout', () => { + expect(typeof deferredJob).toEqual('function') + }) + + describe('workflow: deferredJob -> init -> job -> wait', () => { + const pubSubConfig: RedisConnectionConfig = { + port: 6789, + host: 'localhost', + logger: mockLogger() + } + let pubSub: PubSub + let spySubscribe: jest.SpyInstance + let spyUnsubscribe: jest.SpyInstance + let spyPublish: jest.SpyInstance + const channel = uuidv4() + const publishTimeoutInMs = 50 + beforeEach(async () => { + pubSub = new PubSub(pubSubConfig) + await pubSub.connect() + let notifyCb: NotificationCallback + spySubscribe = jest.spyOn(pubSub, 'subscribe') + .mockImplementation((_channel: string, cb: NotificationCallback) => { + // store callback to be used in `publish` + notifyCb = cb + return 1 // hardcoded sid + }) + spyUnsubscribe = jest.spyOn(pubSub, 'unsubscribe') + .mockImplementation(() => true) // true returned when unsubscribe done + spyPublish = jest.spyOn(pubSub, 'publish') + .mockImplementationOnce((channel: string, message: Message) => { + // invoke stored callback to simulate + setTimeout(() => notifyCb(channel, message, 1), publishTimeoutInMs) + return Promise.resolve() + }) + }) + + afterEach(async () => { + await pubSub.disconnect() + }) + + test('happy flow', async (done) => { + const jobInitiator = jest.fn(() => Promise.resolve()) + const jobListener = jest.fn(() => Promise.resolve()) + const initOrTrigger = deferredJob(pubSub, channel) + + // check workflow layout + expect(typeof initOrTrigger.init).toEqual('function') + expect(typeof initOrTrigger.trigger).toEqual('function') + + const dj = initOrTrigger.init(jobInitiator) + expect(typeof dj.job).toEqual('function') + + const dw = dj.job(jobListener) + expect(typeof dw.wait).toEqual('function') + + // wait phase - execution + dw.wait(publishTimeoutInMs + 10).then(() => { + expect(spyPublish).toHaveBeenCalledWith(channel, { the: 'message' }) + expect(spyUnsubscribe).toHaveBeenCalledWith(channel, 1) + done() + }) + expect(spySubscribe).toHaveBeenCalledWith(channel, expect.any(Function)) + await initOrTrigger.trigger({ the: 'message' }) + }) + + test('timeout', async (done) => { + const jobInitiator = jest.fn(() => Promise.resolve()) + const jobListener = jest.fn(() => Promise.resolve()) + + const dw = deferredJob(pubSub, channel) + .init(jobInitiator) + .job(jobListener) + + // wait phase - set timeout before publish will happen + dw.wait(publishTimeoutInMs - 10).catch((err) => { + expect(err).toBeInstanceOf(TimeoutError) + expect(spyPublish).toHaveBeenCalledWith(channel, { the: 'message' }) + expect(spyUnsubscribe).toHaveBeenCalledWith(channel, 1) + done() + }) + expect(spySubscribe).toHaveBeenCalledWith(channel, expect.any(Function)) + await deferredJob(pubSub, channel).trigger({ the: 'message' }) + }) + + test('exception from jobInitiator', (done) => { + const jobInitiator = jest.fn(() => { throw new Error('job-initiator throws') }) + const jobListener = jest.fn(() => Promise.resolve()) + + const dw = deferredJob(pubSub, channel) + .init(jobInitiator) + .job(jobListener) + + // wait phase - set timeout before publish will happen + dw.wait(publishTimeoutInMs + 10).catch((err) => { + expect(err.message).toEqual('job-initiator throws') + expect(spyPublish).not.toHaveBeenCalled() + expect(spyUnsubscribe).toHaveBeenCalledWith(channel, 1) + done() + }) + expect(spySubscribe).toHaveBeenCalledWith(channel, expect.any(Function)) + }) + + test('exception from jobListener', async (done) => { + const jobInitiator = jest.fn(() => Promise.resolve()) + const jobListener = jest.fn(() => { throw new Error('job-listener throws') }) + + const dw = deferredJob(pubSub, channel) + .init(jobInitiator) + .job(jobListener) + + // wait phase - set timeout before publish will happen + // testing default argument for wait + dw.wait(undefined as unknown as number).catch((err) => { + expect(err.message).toEqual('job-listener throws') + expect(spySubscribe).toHaveBeenCalledWith(channel, expect.any(Function)) + expect(spyUnsubscribe).toHaveBeenCalledWith(channel, 1) + done() + }) + expect(spySubscribe).toHaveBeenCalledWith(channel, expect.any(Function)) + await deferredJob(pubSub, channel).trigger({ the: 'message' }) + }) + + test('input validation', () => { + const jobInitiator = jest.fn(() => Promise.resolve()) + const jobListener = jest.fn(() => Promise.resolve()) + + expect(() => deferredJob(pubSub, channel) + .init(null as unknown as JobInitiator) + ).toThrowError() + + expect(() => deferredJob(pubSub, channel) + .init(jobInitiator) + .job(null as unknown as JobListener) + ).toThrowError() + + expect(deferredJob(pubSub, channel) + .init(jobInitiator) + .job(jobListener) + .wait(-1) + ).rejects.toBeInstanceOf(PositiveTimeoutRequired) + }) + }) +}) diff --git a/test/unit/shared/http-scheme.test.ts b/test/unit/shared/http-scheme.test.ts new file mode 100644 index 00000000..0e0a168f --- /dev/null +++ b/test/unit/shared/http-scheme.test.ts @@ -0,0 +1,45 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + * Paweł Marzec + -------------- + ******/ +import HttpScheme, { Scheme, prependHttp2Uri, prependHttps2Uri } from '~/shared/http-scheme' + +describe('http-scheme', () => { + it('should have proper default layout', () => { + expect(typeof HttpScheme.prepend2Uri).toEqual('function') + expect(typeof HttpScheme.prependHttp2Uri).toEqual('function') + expect(typeof HttpScheme.prependHttps2Uri).toEqual('function') + expect(Scheme.http).toEqual('http') + expect(Scheme.https).toEqual('https') + }) + + it('should append \'http\'', () => { + expect(prependHttp2Uri('uri')).toEqual('http://uri') + }) + + it('should append \'https\'', () => { + expect(prependHttps2Uri('uri')).toEqual('https://uri') + }) +}) diff --git a/test/unit/shared/kvs.test.ts b/test/unit/shared/kvs.test.ts new file mode 100644 index 00000000..e00d430b --- /dev/null +++ b/test/unit/shared/kvs.test.ts @@ -0,0 +1,208 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + +import { Callback } from 'redis' +import { InvalidKeyError, KVS } from '~/shared/kvs' +import { RedisConnectionConfig } from '~/shared/redis-connection' +import mockLogger from '../mockLogger' +import shouldNotBeExecuted from '../shouldNotBeExecuted' + +jest.mock('redis') + +describe('KVS: Key Value Storage', () => { + const config: RedisConnectionConfig = { + port: 6789, + host: 'localhost', + logger: mockLogger() + } + + it('should be well constructed', () => { + const kvs = new KVS(config) + expect(kvs.port).toBe(config.port) + expect(kvs.host).toEqual(config.host) + expect(kvs.logger).toEqual(config.logger) + expect(kvs.isConnected).toBeFalsy() + }) + + it('should GET value', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + + const getSpy = jest.spyOn(kvs.client, 'get').mockImplementationOnce( + (_key: string, cb?: Callback): boolean => { + if (cb) { + cb(null, JSON.stringify({ am: 'the-value' })) + } + return true + } + ) + const result: Record | undefined = await kvs.get('the-key') + expect(result).toEqual({ am: 'the-value' }) + expect(getSpy).toBeCalledTimes(1) + expect(getSpy.mock.calls[0][0]).toEqual('the-key') + }) + + it('should validate empty key for GET', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + try { + await kvs.get('') + } catch (error) { + expect(error).toEqual(new InvalidKeyError()) + } + }) + + it('should validate invalid key for GET', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + try { + await kvs.get(null as unknown as string) + } catch (error) { + expect(error).toEqual(new InvalidKeyError()) + } + }) + + it('should return undefined if there is no value for key', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + + // simulate returning null from kvs.client.get + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + kvs.client.get = jest.fn((key, cb) => cb(null, null)) + const result = await kvs.get('not-existing-key') + expect(result).toBeUndefined() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(kvs.client.get.mock.calls[0][0]).toEqual('not-existing-key') + }) + + it('should SET value', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + + const setSpy = jest.spyOn(kvs.client, 'set').mockImplementationOnce(( + _key: string, + _value: string, + flag: string, + _mode: string, + _duration: number, + _cb?: Callback<'OK' | undefined> + ): boolean => { + if (flag) { + const ccb = flag as unknown as Callback<'OK' | undefined> + ccb(null, 'OK') + } + return true + }) + const result = await kvs.set('the-key', { am: 'the-value' }) + expect(result).toEqual('OK') + expect(setSpy).toBeCalledTimes(1) + expect(setSpy.mock.calls[0][0]).toEqual('the-key') + expect(setSpy.mock.calls[0][1]).toEqual(JSON.stringify({ am: 'the-value' })) + }) + + it('should validate empty key empty for SET', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + try { + await kvs.set('', true) + } catch (error) { + expect(error).toEqual(new InvalidKeyError()) + } + }) + + it('should validate invalid key for SET', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + try { + await kvs.set(null as unknown as string, true) + } catch (error) { + expect(error).toEqual(new InvalidKeyError()) + } + }) + + it('should DEL key', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + const key = 'the-key' + + const delSpy = jest.spyOn(kvs.client, 'del').mockImplementationOnce(( + ...args: (string | Callback)[] + ): boolean => { + const cb = args[1] as Callback + cb(null, 1) + return false + }) + + const result = await kvs.del(key) + expect(result).toBeTruthy() + expect(delSpy).toBeCalledTimes(1) + expect(delSpy).toBeCalledWith(key, expect.anything()) + }) + + it('should validate invalid key for DEL', async (): Promise => { + const kvs = new KVS(config) + await kvs.connect() + try { + await kvs.del(null as unknown as string) + } catch (error) { + expect(error).toEqual(new InvalidKeyError()) + } + }) + + it('should check does key EXISTS', async () => { + const key = 'non-existing-key' + const kvs = new KVS(config) + await kvs.connect() + + const spyExists = jest.spyOn(kvs.client, 'exists') + const result = await kvs.exists(key) + expect(result).toBeFalsy() + expect(spyExists).toBeCalledWith(key, expect.anything()) + }) + + it('should reject and propagate error from EXISTS', async () => { + const key = 'non-existing-key' + const kvs = new KVS(config) + await kvs.connect() + + const spyExists = jest.spyOn(kvs.client, 'exists').mockImplementationOnce((...args: (string | Callback)[] + ): boolean => { + const cb = args[1] as Callback + cb(new Error('mocked-error'), 0) + return false + }) + try { + await kvs.exists(key) + shouldNotBeExecuted() + } catch (err) { + expect(err).toEqual(new Error('mocked-error')) + } + expect(spyExists).toBeCalledWith(key, expect.anything()) + }) +}) diff --git a/test/unit/shared/pub-sub.test.ts b/test/unit/shared/pub-sub.test.ts new file mode 100644 index 00000000..c0324dd8 --- /dev/null +++ b/test/unit/shared/pub-sub.test.ts @@ -0,0 +1,163 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec +-------------- +******/ + +import { + InvalidCallbackIdError, + InvalidChannelNameError, + InvalidMessageError, + Message, + PubSub +} from '~/shared/pub-sub' +import { + RedisConnectionConfig +} from '~/shared/redis-connection' +// import Redis from 'redis' +import mockLogger from '../mockLogger' +jest.mock('redis') + +describe('PubSub', () => { + const config: RedisConnectionConfig = { + port: 6789, + host: 'localhost', + logger: mockLogger() + } + + beforeEach(() => jest.resetAllMocks()) + + it('should be well constructed', () => { + const ps = new PubSub(config) + expect(ps.port).toBe(config.port) + expect(ps.host).toEqual(config.host) + expect(ps.logger).toEqual(config.logger) + expect(ps.isConnected).toBeFalsy() + }) + + it('should connect', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + expect(ps.isConnected).toBeTruthy() + }) + + it('should broadcast message to subscribed notification callbacks', (done): void => { + const ps = new PubSub(config) + ps.connect() + .then(() => { + const notificationCallback = jest.fn() + const id = ps.subscribe('first-channel', notificationCallback) + expect(id).toBe(1) + + // no need to await for this promise + ps.publish('first-channel', { Iam: 'the-message' }) + + setTimeout(() => { + expect(notificationCallback).toBeCalledWith('first-channel', { Iam: 'the-message' }, id) + done() + }, 10) + }) + }) + + it('subscribe should do channel validation', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + expect(() => ps.subscribe('', jest.fn())).toThrowError(new InvalidChannelNameError()) + }) + + it('should unsubscribe', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + const id = ps.subscribe('first-channel', jest.fn()) + expect(id).toBe(1) + + const result = ps.unsubscribe('first-channel', id) + expect(result).toBeTruthy() + }) + + it('unsubscribe should do nothing if wrong channel name', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + const result = ps.unsubscribe('first-channel', 1) + expect(result).toBeFalsy() + }) + + it('unsubscribe should do nothing if wrong callbackId', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + let id = ps.subscribe('first-channel', jest.fn()) + expect(id).toBe(1) + // check more than one subscription to the same channel + id = ps.subscribe('first-channel', jest.fn()) + expect(id).toBe(2) + + const result = ps.unsubscribe('first-channel', id + 1) + expect(result).toBeFalsy() + }) + + it('unsubscribe should do channel validation', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + expect(() => ps.unsubscribe('', 1)).toThrowError(new InvalidChannelNameError()) + }) + + it('unsubscribe should do callbackId validation', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + expect(() => ps.unsubscribe('the-channel', -1)).toThrowError(new InvalidCallbackIdError()) + }) + + it('publish should do channel validation', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + expect(ps.publish('', true)).rejects.toEqual(new InvalidChannelNameError()) + }) + + it('publish should do Message validation', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + expect(ps.publish('the-channel', null as unknown as Message)).rejects.toEqual( + new InvalidMessageError('the-channel') + ) + }) + + it('broadcast should do nothing if no listener registered', async (): Promise => { + const ps = new PubSub(config) + await ps.connect() + + ps.client.emit('message', 'not-existing') + await new Promise((resolve) => { + expect(ps.logger.info).toBeCalledWith('broadcastMessage: no callbacks for \'not-existing\' channel') + setTimeout(resolve, 10, {}) + }) + }) +}) diff --git a/test/unit/shared/redis-connection.test.ts b/test/unit/shared/redis-connection.test.ts new file mode 100644 index 00000000..c253b92e --- /dev/null +++ b/test/unit/shared/redis-connection.test.ts @@ -0,0 +1,237 @@ +/***** + 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 + Gates 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 . + * Gates Foundation + - Name Surname + + - Paweł Marzec + -------------- + ******/ + + import { + InvalidHostError, + InvalidLoggerError, + InvalidPortError, + RedisConnection, + RedisConnectionConfig, + RedisConnectionError +} from '~/shared/redis-connection' +import { Logger as SDKLogger } from '@mojaloop/sdk-standard-components' +import Redis from 'redis' +import mockLogger from '../mockLogger' +jest.mock('redis') + +describe('RedisConnection', () => { + const defaultTimeout = 100 + const config: RedisConnectionConfig = { + port: 6789, + host: 'localhost', + logger: mockLogger(), + timeout: defaultTimeout + } + + beforeEach(() => jest.resetAllMocks()) + + it('should be well constructed', () => { + const redis = new RedisConnection(config) + expect(redis.port).toBe(config.port) + expect(redis.host).toEqual(config.host) + expect(redis.logger).toEqual(config.logger) + expect(redis.isConnected).toBeFalsy() + expect(redis.timeout).toBe(defaultTimeout) + }) + + it('should do input validation for port', () => { + const invalidPort = { ...config } + invalidPort.port = -1 + expect(() => new RedisConnection(invalidPort)).toThrowError(new InvalidPortError()) + }) + + it('should do input validation for host', () => { + const invalidHost = { ...config } + invalidHost.host = '' + expect(() => new RedisConnection(invalidHost)).toThrowError(new InvalidHostError()) + invalidHost.host = null as unknown as string + expect(() => new RedisConnection(invalidHost)).toThrowError(new InvalidHostError()) + }) + + it('should do input validation for logger', () => { + const invalidLogger = { ...config } + invalidLogger.logger = null as unknown as SDKLogger.Logger + expect(() => new RedisConnection(invalidLogger)).toThrowError(new InvalidLoggerError()) + }) + + it('should connect', async (): Promise => { + const redis = new RedisConnection(config) + await redis.connect() + expect(redis.isConnected).toBeTruthy() + expect(config.logger.info).toBeCalledWith(`createClient: Connected to REDIS at: ${config.host}:${config.port}`) + }) + + it('should connect if already connected', async (): Promise => { + const redis = new RedisConnection(config) + await redis.connect() + expect(redis.isConnected).toBeTruthy() + await redis.connect() + expect(redis.isConnected).toBeTruthy() + expect(config.logger.info).toBeCalledWith(`createClient: Connected to REDIS at: ${config.host}:${config.port}`) + }) + + it('should throw if trying to access \'client\' property when not connected ', async (): Promise => { + const redis = new RedisConnection(config) + expect(redis.isConnected).toBeFalsy() + expect(() => redis.client).toThrowError(new RedisConnectionError(config.port, config.host)) + }) + + it('should disconnect when connected', async (): Promise => { + const redis = new RedisConnection(config) + await redis.connect() + expect(redis.isConnected).toBeTruthy() + await redis.disconnect() + expect(redis.isConnected).toBeFalsy() + }) + + it('should do nothing at disconnect when not connected', async (): Promise => { + const redis = new RedisConnection(config) + expect(redis.isConnected).toBeFalsy() + await redis.disconnect() + expect(redis.isConnected).toBeFalsy() + }) + + it('should connect without timeout specified', async (): Promise => { + const configNoTimeout = { ...config } + configNoTimeout.timeout = undefined + + const redis = new RedisConnection(configNoTimeout) + await redis.connect() + expect(redis.timeout).toEqual(RedisConnection.defaultTimeout) + }) + + it('should PING', async (): Promise => { + const redis = new RedisConnection(config) + await redis.connect() + const pong = await redis.ping() + expect(pong).toBe(true) + }) + + it('should handle redis errors', async (): Promise => { + const createClientSpy = jest.spyOn(Redis, 'createClient') + const mockQuit = jest.fn() + createClientSpy.mockImplementationOnce(() => ( + { + quit: mockQuit, + // simulate sending notification on error + on: jest.fn((msg: string, cb: (err: Error | null) => void): void => { + // do nothing on ready because we want to enforce error to reject promise + if (msg === 'ready') { + setTimeout(() => { + cb(null) + }, defaultTimeout + 10) + } + if (msg === 'error') { + setImmediate(() => cb(new Error('emitted'))) + } + }) + } as unknown as Redis.RedisClient + )) + + const redis = new RedisConnection(config) + try { + await redis.connect() + } catch (error) { + expect(error).toEqual(new Error('emitted')) + expect(config.logger.push).toHaveBeenCalledWith({ err: new Error('emitted') }) + expect(config.logger.error).toHaveBeenCalledWith('createClient: Error from REDIS client') + expect(mockQuit).toBeCalledTimes(1) + } + }) + + it('should not reject if reconnection successful', async (): Promise => { + const createClientSpy = jest.spyOn(Redis, 'createClient') + const mockQuit = jest.fn() + createClientSpy.mockImplementationOnce(() => ( + { + quit: mockQuit, + // simulate sending notification on error + on: jest.fn((msg: string, cb: (err: Error | null) => void): void => { + // do nothing on ready because we want to enforce error to reject promise + if (msg === 'ready') { + setTimeout(() => { + cb(null) + }, defaultTimeout - 10) + } + if (msg === 'error') { + setImmediate(() => cb(new Error('emitted'))) + } + }) + } as unknown as Redis.RedisClient + )) + const redis = new RedisConnection(config) + try { + await redis.connect() + } catch (error) { + expect(error).toEqual(new Error('emitted')) + expect(config.logger.push).toHaveBeenCalledWith({ err: new Error('emitted') }) + expect(config.logger.error).toHaveBeenCalledWith('createClient: Error from REDIS client') + expect(mockQuit).toBeCalledTimes(1) + } + }) + + it('should protected against multiple rejection when ready but log errors', (done): void => { + const createClientSpy = jest.spyOn(Redis, 'createClient') + const mockQuit = jest.fn() + createClientSpy.mockImplementationOnce(() => ( + { + quit: mockQuit, + // simulate sending notification on error + on: jest.fn((msg: string, cb: (err: Error | null) => void): void => { + // invoke ready so promise resolve + if (msg === 'ready') { + setImmediate(() => { + expect(config.logger.info).toHaveBeenCalledTimes(0) + cb(null) + expect(config.logger.info).toHaveBeenCalledWith( + `createClient: Connected to REDIS at: ${config.host}:${config.port}` + ) + }) + } + // after invoke ready trigger error to check the promise wasn't rejected + if (msg === 'error') { + setTimeout(() => { + cb(new Error('emitted')) + expect(config.logger.push).toHaveBeenCalledWith({ err: new Error('emitted') }) + expect(config.logger.error).toHaveBeenCalledWith('createClient: Error from REDIS client') + // if promise wasn't reject the quit shouldn't be called + expect(mockQuit).not.toBeCalled() + done() + }, 10) + } + }) + } as unknown as Redis.RedisClient + )) + const redis = new RedisConnection(config) + redis.connect() + .catch(() => { + // fail test if this line is reached + // so proof that multiple rejection doesn't happen + expect(true).toBe(false) + }) + }) +}) diff --git a/test/unit/shouldNotBeExecuted.ts b/test/unit/shouldNotBeExecuted.ts new file mode 100644 index 00000000..0bc4bdb1 --- /dev/null +++ b/test/unit/shouldNotBeExecuted.ts @@ -0,0 +1,3 @@ +export default function shouldNotBeExecuted () { + throw new Error('test failure enforced: this code should never be executed') +} diff --git a/test/unit/sortedArray.ts b/test/unit/sortedArray.ts new file mode 100644 index 00000000..7ec7ad6b --- /dev/null +++ b/test/unit/sortedArray.ts @@ -0,0 +1,5 @@ +export default function sortedArray (a: string[]): string[] { + const b = [...a] + b.sort() + return b +}