From 79795937e64c516d3d4fb4dfc172ff90af5a96a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tayfun=20Y=C4=B1lmaz?= Date: Fri, 24 Apr 2026 10:08:01 +0300 Subject: [PATCH] feat: add jmeter load test and account-opening instance lookup Add a containerized JMeter load test for the async account-opening happy path that polls state between transitions, with overridable base URL and load profile. Also introduce get-instance(s) tasks and an initiate-account-opening schema to support querying existing instances and validating the start transition payload. --- .gitignore | 6 +- README.md | 57 +++ .../multi-task-function-test.json | 2 +- .../src/FunctionValidatePoliciesMapping.csx | 2 +- .../account-type-selection.json | 2 +- .../initiate-account-opening.json | 49 ++ core/Tasks/account-opening/get-instance.json | 18 + core/Tasks/account-opening/get-instances.json | 19 + .../account-opening-workflow.json | 34 +- .../src/GetInstanceByOldKeyMapping.csx | 33 ++ .../src/GetInstancesByOldKeyMapping.csx | 36 ++ docker-compose.test.yml | 30 ++ jmeter/tests/workflow-test.jmx | 437 ++++++++++++++++++ 13 files changed, 720 insertions(+), 5 deletions(-) create mode 100644 core/Schemas/account-opening/initiate-account-opening.json create mode 100644 core/Tasks/account-opening/get-instance.json create mode 100644 core/Tasks/account-opening/get-instances.json create mode 100644 core/Workflows/account-opening/src/GetInstanceByOldKeyMapping.csx create mode 100644 core/Workflows/account-opening/src/GetInstancesByOldKeyMapping.csx create mode 100644 docker-compose.test.yml create mode 100644 jmeter/tests/workflow-test.jmx diff --git a/.gitignore b/.gitignore index cdac152..96484c7 100644 --- a/.gitignore +++ b/.gitignore @@ -271,4 +271,8 @@ auth.json secrets.json # AI folders -ai-docs/ \ No newline at end of file +ai-docs/ + +# JMeter +jmeter/results/ +!jmeter/results/.gitkeep \ No newline at end of file diff --git a/README.md b/README.md index e7c861c..0673524 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,63 @@ dist/ | `npm test` | Run tests | +## πŸ§ͺ Load Testing (JMeter) + +Asynchronous (`sync=false`) load test for the account-opening workflow is executed via `docker-compose.test.yml`. JMeter joins the existing `bbt-development` Docker network and does not modify the main `docker-compose.yml`. + +### Structure + +``` +jmeter/ +β”œβ”€β”€ tests/ +β”‚ └── workflow-test.jmx +└── results/ # gitignored; reports are written here +``` + +### Running + +```bash +# Default: 10 users, 10s ramp-up, 5 loops, base URL http://host.docker.internal:4201 +docker compose -f docker-compose.test.yml up --abort-on-container-exit + +# Custom base URL (e.g. a service name on the same network) +VNEXT_BASE_URL=http://amorphie-workflow:4201 \ + docker compose -f docker-compose.test.yml up --abort-on-container-exit + +# Custom load profile +VNEXT_BASE_URL=http://host.docker.internal:4201 \ +JMETER_USERS=50 JMETER_RAMPUP=30 JMETER_LOOPS=10 \ + docker compose -f docker-compose.test.yml up --abort-on-container-exit + +# Tear down +docker compose -f docker-compose.test.yml down +``` + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `VNEXT_BASE_URL` | `http://host.docker.internal:4201` | vNext workflow engine base URL | +| `JMETER_USERS` | `10` | Number of concurrent threads (users) | +| `JMETER_RAMPUP` | `10` | Ramp-up time in seconds | +| `JMETER_LOOPS` | `5` | Iterations per thread | + +### Scenario + +Single thread group, full happy path: + +1. `POST /instances/start?sync=false` β†’ extract `id` β†’ poll state until `status=A` (max 5 polls, 1s backoff while `B`) +2. `PATCH /transitions/select-demand-deposit?sync=false` β†’ poll until `status=A` +3. `PATCH /transitions/submit-account-details?sync=false` β†’ poll until `status=A` +4. `PATCH /transitions/confirm-account-opening?sync=false` (final transition; response not asserted) + +If the polled status returns `F` (or unexpected `C`) at any intermediate step, the remaining samplers in that iteration are skipped and the next loop starts. + +### Results + +- Raw JTL: `./jmeter/results/result.jtl` +- HTML report: `./jmeter/results/html-report/index.html` + ## 🀝 Contributing 1. Fork the repository diff --git a/core/Functions/account-opening/multi-task-function-test.json b/core/Functions/account-opening/multi-task-function-test.json index abbe21f..459ba98 100644 --- a/core/Functions/account-opening/multi-task-function-test.json +++ b/core/Functions/account-opening/multi-task-function-test.json @@ -25,7 +25,7 @@ }, "mapping": { "location": "./src/FunctionValidatePoliciesMapping.csx", - "code": "dXNpbmcgU3lzdGVtOwp1c2luZyBTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYzsKdXNpbmcgU3lzdGVtLlRocmVhZGluZy5UYXNrczsKdXNpbmcgQkJULldvcmtmbG93LlNjcmlwdGluZzsKdXNpbmcgQkJULldvcmtmbG93LkRlZmluaXRpb25zOwoKLy8vIDxzdW1tYXJ5PgovLy8gVGFzayAxIG1hcHBpbmcgZm9yIG11bHRpLXRhc2sgZnVuY3Rpb24gdGVzdC4KLy8vIENhbGxzIHZhbGlkYXRlLWFjY291bnQtcG9saWNpZXMgSFRUUCB0YXNrIHdpdGggaW5zdGFuY2UgZGF0YS4KLy8vIDwvc3VtbWFyeT4KcHVibGljIGNsYXNzIEZ1bmN0aW9uVmFsaWRhdGVQb2xpY2llc01hcHBpbmcgOiBJTWFwcGluZwp7CiAgICBwdWJsaWMgVGFzazxTY3JpcHRSZXNwb25zZT4gSW5wdXRIYW5kbGVyKFdvcmtmbG93VGFzayB0YXNrLCBTY3JpcHRDb250ZXh0IGNvbnRleHQpCiAgICB7CiAgICAgICAgdmFyIGh0dHBUYXNrID0gdGFzayBhcyBIdHRwVGFzazs7OwogICAgICAgIGlmIChodHRwVGFzayA9PSBudWxsKQogICAgICAgICAgICByZXR1cm4gVGFzay5Gcm9tUmVzdWx0KG5ldyBTY3JpcHRSZXNwb25zZSgpKTsKCiAgICAgICAgdmFyIHVzZXJTZXNzaW9uID0gY29udGV4dC5JbnN0YW5jZT8uRGF0YT8udXNlclNlc3Npb247CiAgICAgICAgdmFyIGFjY291bnRUeXBlID0gY29udGV4dC5JbnN0YW5jZT8uRGF0YT8uYWNjb3VudFR5cGU7CgogICAgICAgIGh0dHBUYXNrLlNldEJvZHkobmV3CiAgICAgICAgewogICAgICAgICAgICB1c2VySWQgPSB1c2VyU2Vzc2lvbj8udXNlcklkLAogICAgICAgICAgICBhY2NvdW50VHlwZSA9IGFjY291bnRUeXBlID8/ICJkZW1hbmQtZGVwb3NpdCIsCiAgICAgICAgICAgIGN1cnJlbmN5ID0gY29udGV4dC5JbnN0YW5jZT8uRGF0YT8uY3VycmVuY3ksCiAgICAgICAgICAgIHJlcXVlc3RlZEF0ID0gRGF0ZVRpbWUuVXRjTm93CiAgICAgICAgfSk7CgogICAgICAgIGh0dHBUYXNrLlNldEhlYWRlcnMobmV3IERpY3Rpb25hcnk8c3RyaW5nLCBzdHJpbmc/PgogICAgICAgIHsKICAgICAgICAgICAgWyJDb250ZW50LVR5cGUiXSA9ICJhcHBsaWNhdGlvbi9qc29uIiwKICAgICAgICAgICAgWyJYLUluc3RhbmNlLUlkIl0gPSBjb250ZXh0Lkluc3RhbmNlPy5JZC5Ub1N0cmluZygpLAogICAgICAgICAgICBbIlgtUmVxdWVzdC1JZCJdID0gY29udGV4dC5IZWFkZXJzP1sieC1yZXF1ZXN0LWlkIl0gPz8gR3VpZC5OZXdHdWlkKCkuVG9TdHJpbmcoKQogICAgICAgIH0pOwoKICAgICAgICByZXR1cm4gVGFzay5Gcm9tUmVzdWx0KG5ldyBTY3JpcHRSZXNwb25zZSgpKTsKICAgIH0KCiAgICBwdWJsaWMgVGFzazxTY3JpcHRSZXNwb25zZT4gT3V0cHV0SGFuZGxlcihTY3JpcHRDb250ZXh0IGNvbnRleHQpCiAgICB7CiAgICAgICAgcmV0dXJuIFRhc2suRnJvbVJlc3VsdChuZXcgU2NyaXB0UmVzcG9uc2UKICAgICAgICB7CiAgICAgICAgICAgIEtleSA9ICJwb2xpY3ktY2hlY2stcmVzdWx0IiwKICAgICAgICAgICAgRGF0YSA9IGNvbnRleHQuQm9keQogICAgICAgIH0pOwogICAgfQp9Cg==" + "code": "dXNpbmcgU3lzdGVtOwp1c2luZyBTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYzsKdXNpbmcgU3lzdGVtLlRocmVhZGluZy5UYXNrczsKdXNpbmcgQkJULldvcmtmbG93LlNjcmlwdGluZzsKdXNpbmcgQkJULldvcmtmbG93LkRlZmluaXRpb25zOwoKLy8vIDxzdW1tYXJ5PgovLy8gVGFzayAxIG1hcHBpbmcgZm9yIG11bHRpLXRhc2sgZnVuY3Rpb24gdGVzdC4KLy8vIENhbGxzIHZhbGlkYXRlLWFjY291bnQtcG9saWNpZXMgSFRUUCB0YXNrIHdpdGggaW5zdGFuY2UgZGF0YS4KLy8vIDwvc3VtbWFyeT4KcHVibGljIGNsYXNzIEZ1bmN0aW9uVmFsaWRhdGVQb2xpY2llc01hcHBpbmcgOiBJTWFwcGluZwp7CiAgICBwdWJsaWMgVGFzazxTY3JpcHRSZXNwb25zZT4gSW5wdXRIYW5kbGVyKFdvcmtmbG93VGFzayB0YXNrLCBTY3JpcHRDb250ZXh0IGNvbnRleHQpCiAgICB7CiAgICAgICAgdmFyIGh0dHBUYXNrID0gdGFzayBhcyBIdHRwVGFzazsKICAgICAgICBpZiAoaHR0cFRhc2sgPT0gbnVsbCkKICAgICAgICAgICAgcmV0dXJuIFRhc2suRnJvbVJlc3VsdChuZXcgU2NyaXB0UmVzcG9uc2UoKSk7CgogICAgICAgIHZhciB1c2VyU2Vzc2lvbiA9IGNvbnRleHQuSW5zdGFuY2U/LkRhdGE/LnVzZXJTZXNzaW9uOwogICAgICAgIHZhciBhY2NvdW50VHlwZSA9IGNvbnRleHQuSW5zdGFuY2U/LkRhdGE/LmFjY291bnRUeXBlOwoKICAgICAgICBodHRwVGFzay5TZXRCb2R5KG5ldwogICAgICAgIHsKICAgICAgICAgICAgdXNlcklkID0gdXNlclNlc3Npb24/LnVzZXJJZCwKICAgICAgICAgICAgYWNjb3VudFR5cGUgPSBhY2NvdW50VHlwZSA/PyAiZGVtYW5kLWRlcG9zaXQiLAogICAgICAgICAgICBjdXJyZW5jeSA9IGNvbnRleHQuSW5zdGFuY2U/LkRhdGE/LmN1cnJlbmN5LAogICAgICAgICAgICByZXF1ZXN0ZWRBdCA9IERhdGVUaW1lLlV0Y05vdwogICAgICAgIH0pOwoKICAgICAgICBodHRwVGFzay5TZXRIZWFkZXJzKG5ldyBEaWN0aW9uYXJ5PHN0cmluZywgc3RyaW5nPz4KICAgICAgICB7CiAgICAgICAgICAgIFsiQ29udGVudC1UeXBlIl0gPSAiYXBwbGljYXRpb24vanNvbiIsCiAgICAgICAgICAgIFsiWC1JbnN0YW5jZS1JZCJdID0gY29udGV4dC5JbnN0YW5jZT8uSWQuVG9TdHJpbmcoKSwKICAgICAgICAgICAgWyJYLVJlcXVlc3QtSWQiXSA9IGNvbnRleHQuSGVhZGVycz9bIngtcmVxdWVzdC1pZCJdID8/IEd1aWQuTmV3R3VpZCgpLlRvU3RyaW5nKCkKICAgICAgICB9KTsKCiAgICAgICAgcmV0dXJuIFRhc2suRnJvbVJlc3VsdChuZXcgU2NyaXB0UmVzcG9uc2UoKSk7CiAgICB9CgogICAgcHVibGljIFRhc2s8U2NyaXB0UmVzcG9uc2U+IE91dHB1dEhhbmRsZXIoU2NyaXB0Q29udGV4dCBjb250ZXh0KQogICAgewogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlCiAgICAgICAgewogICAgICAgICAgICBLZXkgPSAicG9saWN5LWNoZWNrLXJlc3VsdCIsCiAgICAgICAgICAgIERhdGEgPSBjb250ZXh0LkJvZHkKICAgICAgICB9KTsKICAgIH0KfQo=" } }, { diff --git a/core/Functions/account-opening/src/FunctionValidatePoliciesMapping.csx b/core/Functions/account-opening/src/FunctionValidatePoliciesMapping.csx index 7e18a1f..a934123 100644 --- a/core/Functions/account-opening/src/FunctionValidatePoliciesMapping.csx +++ b/core/Functions/account-opening/src/FunctionValidatePoliciesMapping.csx @@ -12,7 +12,7 @@ public class FunctionValidatePoliciesMapping : IMapping { public Task InputHandler(WorkflowTask task, ScriptContext context) { - var httpTask = task as HttpTask;;; + var httpTask = task as HttpTask; if (httpTask == null) return Task.FromResult(new ScriptResponse()); diff --git a/core/Schemas/account-opening/account-type-selection.json b/core/Schemas/account-opening/account-type-selection.json index 28a4951..d4220d4 100644 --- a/core/Schemas/account-opening/account-type-selection.json +++ b/core/Schemas/account-opening/account-type-selection.json @@ -46,7 +46,7 @@ ] } }, - "additionalProperties": false + "additionalProperties": true } } } \ No newline at end of file diff --git a/core/Schemas/account-opening/initiate-account-opening.json b/core/Schemas/account-opening/initiate-account-opening.json new file mode 100644 index 0000000..784ea46 --- /dev/null +++ b/core/Schemas/account-opening/initiate-account-opening.json @@ -0,0 +1,49 @@ +{ + "key": "initiate-account-opening", + "version": "1.0.0", + "domain": "core", + "flow": "sys-schemas", + "flowVersion": "1.0.0", + "tags": [ + "banking", + "account-opening", + "start-transition", + "input-schema" + ], + "attributes": { + "type": "workflow", + "schema": { + "$id": "https://schemas.vnext.com/banking/initiate-account-opening.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Initiate Account Opening Schema", + "description": "Schema for the start transition payload of the account-opening workflow", + "type": "object", + "required": [ + "session" + ], + "properties": { + "session": { + "type": "string", + "title": "Session", + "description": "Unique session identifier for the account opening request", + "minLength": 1 + }, + "customer": { + "type": "object", + "title": "Customer", + "description": "Customer information for the account opening request", + "properties": { + "ownerUserId": { + "type": "string", + "title": "Owner User ID", + "description": "Identifier of the user who owns the account opening request", + "minLength": 1 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + } +} diff --git a/core/Tasks/account-opening/get-instance.json b/core/Tasks/account-opening/get-instance.json new file mode 100644 index 0000000..8dcfe91 --- /dev/null +++ b/core/Tasks/account-opening/get-instance.json @@ -0,0 +1,18 @@ +{ + "key": "get-instance", + "version": "1.0.0", + "domain": "core", + "flow": "sys-tasks", + "flowVersion": "1.0.0", + "tags": [ + "account-opening", + "get-instance" + ], + "attributes": { + "type": "13", + "config": { + "domain": "core", + "flow": "account-opening" + } + } +} diff --git a/core/Tasks/account-opening/get-instances.json b/core/Tasks/account-opening/get-instances.json new file mode 100644 index 0000000..97dbabd --- /dev/null +++ b/core/Tasks/account-opening/get-instances.json @@ -0,0 +1,19 @@ +{ + "key": "get-instances", + "version": "1.0.0", + "domain": "core", + "flow": "sys-tasks", + "flowVersion": "1.0.0", + "tags": [ + "account-opening", + "get-instances", + "filter" + ], + "attributes": { + "type": "15", + "config": { + "domain": "core", + "flow": "account-opening" + } + } +} diff --git a/core/Workflows/account-opening/account-opening-workflow.json b/core/Workflows/account-opening/account-opening-workflow.json index 729722e..7d1bc66 100644 --- a/core/Workflows/account-opening/account-opening-workflow.json +++ b/core/Workflows/account-opening/account-opening-workflow.json @@ -19,7 +19,7 @@ "versionStrategy": "Minor", "timer": { "reset": "OnEntry", - "duration": "PT1M" + "duration": "PT15M" } }, "labels": [ @@ -84,6 +84,12 @@ "label": "Hesap AΓ§Δ±lışınΔ± Başlat" } ], + "schema": { + "key": "initiate-account-opening", + "version": "1.0.0", + "domain": "core", + "flow": "sys-schemas" + }, "onExecutionTasks": [ { "order": 1, @@ -192,6 +198,32 @@ "location": "./src/TriggerGetInstanceTaskMapping.csx", "code": "dXNpbmcgU3lzdGVtLlRocmVhZGluZy5UYXNrczsKdXNpbmcgQkJULldvcmtmbG93LlNjcmlwdGluZzsKdXNpbmcgU3lzdGVtLlRleHQuSnNvbjsKCnB1YmxpYyBjbGFzcyBUcmlnZ2VyR2V0SW5zdGFuY2VUYXNrTWFwcGluZyA6IElNYXBwaW5nCnsKICAgIHB1YmxpYyBUYXNrPFNjcmlwdFJlc3BvbnNlPiBJbnB1dEhhbmRsZXIoV29ya2Zsb3dUYXNrIHRhc2ssIFNjcmlwdENvbnRleHQgY29udGV4dCkKICAgIHsKICAgICAgICB2YXIgdHJpZ2dlclRhc2sgPSAodGFzayBhcyBHZXRJbnN0YW5jZURhdGFUYXNrKSE7CiAgICAgICAKICAgICAgICB0cmlnZ2VyVGFzay5TZXRJbnN0YW5jZShjb250ZXh0Lkluc3RhbmNlLktleSk7CiAgICAgICAgCiAgICAgICAgcmV0dXJuIFRhc2suRnJvbVJlc3VsdChuZXcgU2NyaXB0UmVzcG9uc2UoKSk7CiAgICB9CgogICAgcHVibGljIFRhc2s8U2NyaXB0UmVzcG9uc2U+IE91dHB1dEhhbmRsZXIoU2NyaXB0Q29udGV4dCBjb250ZXh0KQogICAgewoKICAgICAgICByZXR1cm4gVGFzay5Gcm9tUmVzdWx0KG5ldyBTY3JpcHRSZXNwb25zZSgpCiAgICAgICAgewogICAgICAgICAgICBEYXRhID0gbmV3CiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgIG9sZEluc3RhbmNlPWNvbnRleHQuQm9keSwKICAgICAgICAgICAgICAgIHN1Y2Nlc3MgPSB0cnVlCiAgICAgICAgICAgIH0KICAgICAgICB9KTsKICAgIH0KfQoKcHVibGljIGNsYXNzIFJlcXVlc3RNb2RlbAp7CiAgICBwdWJsaWMgc3RyaW5nIGtleSB7IGdldDsgc2V0OyB9CiAgICBwdWJsaWMgZHluYW1pYyAgYXR0cmlidXRlcyB7IGdldDsgc2V0OyB9Cn0=" } + }, + { + "order": 2, + "task": { + "key": "get-instances", + "domain": "core", + "version": "1.0.0", + "flow": "sys-tasks" + }, + "mapping": { + "location": "./src/GetInstancesByOldKeyMapping.csx", + "code": "dXNpbmcgU3lzdGVtLlRocmVhZGluZy5UYXNrczsKdXNpbmcgQkJULldvcmtmbG93LlNjcmlwdGluZzsKdXNpbmcgQkJULldvcmtmbG93LkRlZmluaXRpb25zOwoKLy8vIDxzdW1tYXJ5PgovLy8gRmlsdGVycyB3b3JrZmxvdyBpbnN0YW5jZXMgYnkgdGhlICJvbGRLZXkiIHZhbHVlIHNlbnQgaW4gdGhlIHRyYW5zaXRpb24gYm9keS4KLy8vIDwvc3VtbWFyeT4KcHVibGljIGNsYXNzIEdldEluc3RhbmNlc0J5T2xkS2V5TWFwcGluZyA6IElNYXBwaW5nCnsKICAgIHB1YmxpYyBUYXNrPFNjcmlwdFJlc3BvbnNlPiBJbnB1dEhhbmRsZXIoV29ya2Zsb3dUYXNrIHRhc2ssIFNjcmlwdENvbnRleHQgY29udGV4dCkKICAgIHsKICAgICAgICB2YXIgaW5zdGFuY2VzVGFzayA9ICh0YXNrIGFzIEdldEluc3RhbmNlc1Rhc2spITsKCiAgICAgICAgc3RyaW5nPyBvbGRLZXkgPSBjb250ZXh0LkJvZHk/Lm9sZEtleT8uVG9TdHJpbmcoKTsKCiAgICAgICAgaW5zdGFuY2VzVGFzay5TZXRGaWx0ZXIobmV3CiAgICAgICAgewogICAgICAgICAgICBrZXkgPSBuZXcgeyBlcSA9IG9sZEtleSB9CiAgICAgICAgfSk7CgogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlKCkpOwogICAgfQoKICAgIHB1YmxpYyBUYXNrPFNjcmlwdFJlc3BvbnNlPiBPdXRwdXRIYW5kbGVyKFNjcmlwdENvbnRleHQgY29udGV4dCkKICAgIHsKICAgICAgICByZXR1cm4gVGFzay5Gcm9tUmVzdWx0KG5ldyBTY3JpcHRSZXNwb25zZQogICAgICAgIHsKICAgICAgICAgICAgS2V5ID0gImdldC1pbnN0YW5jZXMtYnktb2xkLWtleS1yZXN1bHQiLAogICAgICAgICAgICBEYXRhID0gbmV3CiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgIGluc3RhbmNlcyA9IGNvbnRleHQuQm9keSwKICAgICAgICAgICAgICAgIGZldGNoZWRBdCA9IFN5c3RlbS5EYXRlVGltZS5VdGNOb3cKICAgICAgICAgICAgfQogICAgICAgIH0pOwogICAgfQp9Cg==" + } + }, + { + "order": 3, + "task": { + "key": "get-instance", + "domain": "core", + "version": "1.0.0", + "flow": "sys-tasks" + }, + "mapping": { + "location": "./src/GetInstanceByOldKeyMapping.csx", + "code": "dXNpbmcgU3lzdGVtLlRocmVhZGluZy5UYXNrczsKdXNpbmcgQkJULldvcmtmbG93LlNjcmlwdGluZzsKdXNpbmcgQkJULldvcmtmbG93LkRlZmluaXRpb25zOwoKLy8vIDxzdW1tYXJ5PgovLy8gUmV0cmlldmVzIGEgc2luZ2xlIHdvcmtmbG93IGluc3RhbmNlIGJ5IHRoZSAib2xkS2V5IiB2YWx1ZSBzZW50IGluIHRoZSB0cmFuc2l0aW9uIGJvZHkuCi8vLyA8L3N1bW1hcnk+CnB1YmxpYyBjbGFzcyBHZXRJbnN0YW5jZUJ5T2xkS2V5TWFwcGluZyA6IElNYXBwaW5nCnsKICAgIHB1YmxpYyBUYXNrPFNjcmlwdFJlc3BvbnNlPiBJbnB1dEhhbmRsZXIoV29ya2Zsb3dUYXNrIHRhc2ssIFNjcmlwdENvbnRleHQgY29udGV4dCkKICAgIHsKICAgICAgICB2YXIgaW5zdGFuY2VUYXNrID0gKHRhc2sgYXMgR2V0SW5zdGFuY2VEYXRhVGFzaykhOwoKICAgICAgICBzdHJpbmc/IG9sZEtleSA9IGNvbnRleHQuQm9keT8ub2xkS2V5Py5Ub1N0cmluZygpOwoKICAgICAgICBpbnN0YW5jZVRhc2suU2V0SW5zdGFuY2Uob2xkS2V5KTsKCiAgICAgICAgcmV0dXJuIFRhc2suRnJvbVJlc3VsdChuZXcgU2NyaXB0UmVzcG9uc2UoKSk7CiAgICB9CgogICAgcHVibGljIFRhc2s8U2NyaXB0UmVzcG9uc2U+IE91dHB1dEhhbmRsZXIoU2NyaXB0Q29udGV4dCBjb250ZXh0KQogICAgewogICAgICAgIHJldHVybiBUYXNrLkZyb21SZXN1bHQobmV3IFNjcmlwdFJlc3BvbnNlCiAgICAgICAgewogICAgICAgICAgICBLZXkgPSAiZ2V0LWluc3RhbmNlLWJ5LW9sZC1rZXktcmVzdWx0IiwKICAgICAgICAgICAgRGF0YSA9IG5ldwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgICBpbnN0YW5jZSA9IGNvbnRleHQuQm9keSwKICAgICAgICAgICAgICAgIGZldGNoZWRBdCA9IFN5c3RlbS5EYXRlVGltZS5VdGNOb3cKICAgICAgICAgICAgfQogICAgICAgIH0pOwogICAgfQp9Cg==" + } } ] }, diff --git a/core/Workflows/account-opening/src/GetInstanceByOldKeyMapping.csx b/core/Workflows/account-opening/src/GetInstanceByOldKeyMapping.csx new file mode 100644 index 0000000..054ca29 --- /dev/null +++ b/core/Workflows/account-opening/src/GetInstanceByOldKeyMapping.csx @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using BBT.Workflow.Scripting; +using BBT.Workflow.Definitions; + +/// +/// Retrieves a single workflow instance by the "oldKey" value sent in the transition body. +/// +public class GetInstanceByOldKeyMapping : IMapping +{ + public Task InputHandler(WorkflowTask task, ScriptContext context) + { + var instanceTask = (task as GetInstanceDataTask)!; + + string? oldKey = context.Body?.oldKey?.ToString(); + + instanceTask.SetInstance(oldKey); + + return Task.FromResult(new ScriptResponse()); + } + + public Task OutputHandler(ScriptContext context) + { + return Task.FromResult(new ScriptResponse + { + Key = "get-instance-by-old-key-result", + Data = new + { + instance = context.Body, + fetchedAt = System.DateTime.UtcNow + } + }); + } +} diff --git a/core/Workflows/account-opening/src/GetInstancesByOldKeyMapping.csx b/core/Workflows/account-opening/src/GetInstancesByOldKeyMapping.csx new file mode 100644 index 0000000..2830517 --- /dev/null +++ b/core/Workflows/account-opening/src/GetInstancesByOldKeyMapping.csx @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using BBT.Workflow.Scripting; +using BBT.Workflow.Definitions; + +/// +/// Filters workflow instances by the "oldKey" value sent in the transition body. +/// +public class GetInstancesByOldKeyMapping : IMapping +{ + public Task InputHandler(WorkflowTask task, ScriptContext context) + { + var instancesTask = (task as GetInstancesTask)!; + + string? oldKey = context.Body?.oldKey?.ToString(); + + instancesTask.SetFilter(new + { + key = new { eq = oldKey } + }); + + return Task.FromResult(new ScriptResponse()); + } + + public Task OutputHandler(ScriptContext context) + { + return Task.FromResult(new ScriptResponse + { + Key = "get-instances-by-old-key-result", + Data = new + { + instances = context.Body, + fetchedAt = System.DateTime.UtcNow + } + }); + } +} diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..18d531d --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,30 @@ +networks: + bbt-development: + external: true + +services: + jmeter: + image: alpine/jmeter:latest + container_name: jmeter + networks: + - bbt-development + environment: + - VNEXT_BASE_URL=${VNEXT_BASE_URL:-http://host.docker.internal:4201} + - JMETER_USERS=${JMETER_USERS:-10} + - JMETER_RAMPUP=${JMETER_RAMPUP:-10} + - JMETER_LOOPS=${JMETER_LOOPS:-5} + volumes: + - ./jmeter/tests:/tests + - ./jmeter/results:/results + working_dir: /tests + extra_hosts: + - "host.docker.internal:host-gateway" + entrypoint: ["jmeter"] + command: > + -n -t /tests/workflow-test.jmx + -Jbase.url=${VNEXT_BASE_URL:-http://host.docker.internal:4201} + -Jusers=${JMETER_USERS:-10} + -Jrampup=${JMETER_RAMPUP:-10} + -Jloops=${JMETER_LOOPS:-5} + -l /results/result.jtl + -e -o /results/html-report diff --git a/jmeter/tests/workflow-test.jmx b/jmeter/tests/workflow-test.jmx new file mode 100644 index 0000000..582aad6 --- /dev/null +++ b/jmeter/tests/workflow-test.jmx @@ -0,0 +1,437 @@ + + + + + Account-opening workflow async (sync=false) load test. Polls state until status=A (max 5), final step expects status=C. + false + false + + + + base.url + ${__P(base.url,http://host.docker.internal:4201)} + = + + + users + ${__P(users,10)} + = + + + rampup + ${__P(rampup,10)} + = + + + loops + ${__P(loops,5)} + = + + + + + + + + + continue + + false + ${loops} + + ${users} + ${rampup} + false + + + false + + + + + + groovy + + + true + import java.util.UUID +def rnd = new Random() +def sb = new StringBuilder() +17.times { sb.append(rnd.nextInt(10)) } +vars.put("key", sb.toString()) +vars.put("requestId", UUID.randomUUID().toString()) +vars.put("status", "") +vars.put("pollCount", "0") + +def baseUrl = vars.get("base.url") ?: props.get("base.url") ?: "http://host.docker.internal:4201" +def u = new java.net.URL(baseUrl) +vars.put("protocol", u.getProtocol()) +vars.put("host", u.getHost()) +vars.put("port", (u.getPort() == -1 ? (u.getProtocol() == "https" ? "443" : "80") : u.getPort()).toString()) + + + + + + + true + + + + false + { + "key": "${key}", + "attributes": { + "session": "16", + "customer": { + "ownerUserId": "34987491780" + } + } +} + = + + + + ${protocol} + ${host} + ${port} + /api/v1/core/workflows/account-opening/instances/start?sync=false + POST + true + false + true + false + + + + + Content-Typeapplication/json + Accept-Languagetr-TR + X-Request-Id${requestId} + X-Device-Id${requestId} + X-Token-Id${requestId} + X-Device-InfoIOS 16 + X-Forwarded-For192.168.2.1 + user_reference34987491018 + sub34987491018 + act_sub34987491780 + rolemorph-idm.viewers + + + + + instanceId + $.id + 1 + NOT_FOUND + + + + + + + groovy + vars.put("pollCount","0"); vars.put("status",""); + true + + + + ${__groovy(!"A".equals(vars.get("status")) && !"F".equals(vars.get("status")) && (vars.get("pollCount") as Integer) < 5)} + + + + ${protocol} + ${host} + ${port} + /api/v1/core/workflows/account-opening/instances/${instanceId}/functions/state + GET + true + true + + + + + + sub349874917801 + act_sub34987491781 + + + + + status + $.status + 1 + + + + + groovy + def c = (vars.get("pollCount") as Integer) + 1 +vars.put("pollCount", c.toString()) +def s = vars.get("status") +if ("B".equals(s)) { Thread.sleep(1000) } +if ("F".equals(s) || "C".equals(s)) { ctx.setRestartNextLoop(true) } + + true + + + + + + + true + + + + false + { + "tags": ["test","banking","account-openning","key-test"], + "attributes": { + "accountType": "demand-deposit", + "oldKey": "${key}" + } +} + = + + + + ${protocol} + ${host} + ${port} + /api/v1/core/workflows/account-opening/instances/${instanceId}/transitions/select-demand-deposit?sync=false + PATCH + true + true + + + + + Content-Typeapplication/json + sub34987491018 + act_sub56467491010 + rolemorph-idm.select + + + + + + + + groovy + vars.put("pollCount","0"); vars.put("status",""); + true + + + + ${__groovy(!"A".equals(vars.get("status")) && !"F".equals(vars.get("status")) && (vars.get("pollCount") as Integer) < 5)} + + + + ${protocol} + ${host} + ${port} + /api/v1/core/workflows/account-opening/instances/${instanceId}/functions/state + GET + true + + + + + + sub349874917801 + act_sub34987491781 + + + + + status + $.status + 1 + + + + + groovy + def c = (vars.get("pollCount") as Integer) + 1 +vars.put("pollCount", c.toString()) +def s = vars.get("status") +if ("B".equals(s)) { Thread.sleep(1000) } +if ("F".equals(s) || "C".equals(s)) { ctx.setRestartNextLoop(true) } + + true + + + + + + + true + + + + false + { + "attributes": { + "accountName": "demand-deposit", + "currency": "SLV", + "branchCode": "5436", + "initialDeposit": 100, + "accountPurpose": "personal-banking", + "notifications": { + "smsNotifications": true, + "emailNotifications": true, + "pushNotifications": true + } + } +} + = + + + + ${protocol} + ${host} + ${port} + /api/v1/core/workflows/account-opening/instances/${instanceId}/transitions/submit-account-details?sync=false + PATCH + true + true + + + + + Content-Typeapplication/json + sub34987491018 + act_sub56467491011 + rolemorph-idm.approver + + + + + + + + groovy + vars.put("pollCount","0"); vars.put("status",""); + true + + + + ${__groovy(!"A".equals(vars.get("status")) && !"F".equals(vars.get("status")) && (vars.get("pollCount") as Integer) < 5)} + + + + ${protocol} + ${host} + ${port} + /api/v1/core/workflows/account-opening/instances/${instanceId}/functions/state + GET + true + + + + + + sub349874917801 + act_sub34987491781 + + + + + status + $.status + 1 + + + + + groovy + def c = (vars.get("pollCount") as Integer) + 1 +vars.put("pollCount", c.toString()) +def s = vars.get("status") +if ("B".equals(s)) { Thread.sleep(1000) } +if ("F".equals(s) || "C".equals(s)) { ctx.setRestartNextLoop(true) } + + true + + + + + + + true + + + + false + { + "attributes": { + "confirmed": true, + "termsAccepted": true, + "privacyPolicyAccepted": true + } +} + = + + + + ${protocol} + ${host} + ${port} + /api/v1/core/workflows/account-opening/instances/${instanceId}/transitions/confirm-account-opening?sync=false + PATCH + true + true + + + + + Content-Typeapplication/json + + + + + + + + false + + saveConfig + + truetruetrue + truetruetrue + truefalsetrue + truefalsefalse + falsetruefalse + falsefalse + true + 0truetrue + truetruetruetrue + + + + + + + false + + saveConfig + + truetruetrue + truetruetrue + truefalsetrue + truefalsefalse + falsetruefalse + falsefalse + true + 0truetrue + truetruetruetrue + + + + + + + + + +