first commit
Playwright API test suite for payment/cashier endpoints, with per-environment config (dev/staging/prod) loaded via dotenv. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
23
.env
Normal file
23
.env
Normal file
@@ -0,0 +1,23 @@
|
||||
# API Configuration
|
||||
API_BASE_URL=https://www.babinnovations.com
|
||||
API_USERNAME=ahmedmonire@gmail.com
|
||||
API_PASSWORD=Mav@1234
|
||||
CASHIER_USERNAME=
|
||||
CASHIER_PASSWORD=
|
||||
|
||||
# Dashboard Configuration
|
||||
DASHBOARD_BASE_URL=
|
||||
DASHBOARD_USERNAME=
|
||||
DASHBOARD_PASSWORD=
|
||||
|
||||
# ADB Configuration (Android Debug Bridge)
|
||||
ADB_DEVICE_ID=emulator-5554
|
||||
POS_OK_BUTTON_X=579
|
||||
POS_OK_BUTTON_Y=1256
|
||||
POS_CANCEL_BUTTON_X=400
|
||||
POS_CANCEL_BUTTON_Y=1256
|
||||
|
||||
# Test Configuration
|
||||
TERMINAL_ID=1560127620231203
|
||||
MERCHANT_ID=MERCHANT001
|
||||
TEST_AMOUNT=400
|
||||
23
.env.dev
Normal file
23
.env.dev
Normal file
@@ -0,0 +1,23 @@
|
||||
# API Configuration
|
||||
API_BASE_URL=
|
||||
API_USERNAME=
|
||||
API_PASSWORD=
|
||||
CASHIER_USERNAME=
|
||||
CASHIER_PASSWORD=
|
||||
|
||||
# Dashboard Configuration
|
||||
DASHBOARD_BASE_URL=
|
||||
DASHBOARD_USERNAME=
|
||||
DASHBOARD_PASSWORD=
|
||||
|
||||
# ADB Configuration (Android Debug Bridge)
|
||||
ADB_DEVICE_ID=emulator-5554
|
||||
POS_OK_BUTTON_X=579
|
||||
POS_OK_BUTTON_Y=1256
|
||||
POS_CANCEL_BUTTON_X=400
|
||||
POS_CANCEL_BUTTON_Y=1256
|
||||
|
||||
# Test Configuration
|
||||
TERMINAL_ID=
|
||||
MERCHANT_ID=MERCHANT001
|
||||
TEST_AMOUNT=100
|
||||
23
.env.prod
Normal file
23
.env.prod
Normal file
@@ -0,0 +1,23 @@
|
||||
# API Configuration
|
||||
API_BASE_URL=
|
||||
API_USERNAME=
|
||||
API_PASSWORD=
|
||||
CASHIER_USERNAME=
|
||||
CASHIER_PASSWORD=
|
||||
|
||||
# Dashboard Configuration
|
||||
DASHBOARD_BASE_URL=
|
||||
DASHBOARD_USERNAME=
|
||||
DASHBOARD_PASSWORD=
|
||||
|
||||
# ADB Configuration (Android Debug Bridge)
|
||||
ADB_DEVICE_ID=emulator-5554
|
||||
POS_OK_BUTTON_X=579
|
||||
POS_OK_BUTTON_Y=1256
|
||||
POS_CANCEL_BUTTON_X=400
|
||||
POS_CANCEL_BUTTON_Y=1256
|
||||
|
||||
# Test Configuration
|
||||
TERMINAL_ID=
|
||||
MERCHANT_ID=MERCHANT001
|
||||
TEST_AMOUNT=100
|
||||
23
.env.staging
Normal file
23
.env.staging
Normal file
@@ -0,0 +1,23 @@
|
||||
# API Configuration
|
||||
API_BASE_URL=https://stagingenv.babinnovations.com
|
||||
API_USERNAME=ahmedalnukhalah@gmail.com
|
||||
API_PASSWORD=Mav@1234
|
||||
CASHIER_USERNAME=
|
||||
CASHIER_PASSWORD=
|
||||
|
||||
# Dashboard Configuration
|
||||
DASHBOARD_BASE_URL=
|
||||
DASHBOARD_USERNAME=
|
||||
DASHBOARD_PASSWORD=
|
||||
|
||||
# ADB Configuration (Android Debug Bridge)
|
||||
ADB_DEVICE_ID=emulator-5554
|
||||
POS_OK_BUTTON_X=579
|
||||
POS_OK_BUTTON_Y=1256
|
||||
POS_CANCEL_BUTTON_X=400
|
||||
POS_CANCEL_BUTTON_Y=1256
|
||||
|
||||
# Test Configuration
|
||||
TERMINAL_ID=1560127620231203
|
||||
MERCHANT_ID=MERCHANT001
|
||||
TEST_AMOUNT=100
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
playwright-report/
|
||||
test-results/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
103
README.md
Normal file
103
README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# NeoPaaS E2E API Tests
|
||||
|
||||
End-to-end API test suite for the NeoPaaS platform. Tests all 14 core endpoints in a complete transaction flow (login → purchase → lookups → menu → reset → refund).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ installed
|
||||
- A NeoPaaS account (merchant credentials) for the environment you want to test
|
||||
|
||||
## Setup
|
||||
|
||||
From this folder, install dependencies and the Playwright browser:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
## Environments
|
||||
|
||||
The suite ships with config files for each environment. Open the one you want and update the credentials/terminal as needed:
|
||||
|
||||
| File | Environment | API URL |
|
||||
|----------------|-------------|----------------------------------|
|
||||
| `.env` | Default | https://www.babinnovations.com |
|
||||
| `.env.dev` | Development | https://devpro.babinnovations.com|
|
||||
| `.env.staging` | Staging | https://devpro.babinnovations.com|
|
||||
| `.env.prod` | Production | https://www.babinnovations.com |
|
||||
|
||||
Each file holds the API URL, login credentials, terminal ID, and test amount:
|
||||
|
||||
```dotenv
|
||||
API_BASE_URL=https://www.babinnovations.com
|
||||
API_USERNAME=your-email@example.com
|
||||
API_PASSWORD=your-password
|
||||
TERMINAL_ID=1560127620231203
|
||||
TEST_AMOUNT=400
|
||||
```
|
||||
|
||||
> Tip: copy `.env.example` to create your own env file if you need a custom target.
|
||||
|
||||
## Running the tests
|
||||
|
||||
Pick the environment you want to run against:
|
||||
|
||||
```bash
|
||||
npm test # uses .env (default)
|
||||
npm run test:dev # uses .env.dev
|
||||
npm run test:staging # uses .env.staging
|
||||
npm run test:prod # uses .env.prod
|
||||
```
|
||||
|
||||
|
||||
View the HTML report after a run:
|
||||
|
||||
|
||||
```bash
|
||||
npm run report
|
||||
```
|
||||
|
||||
When the test starts, it prints which env file was loaded and the API URL so you always know where it's pointing:
|
||||
|
||||
```
|
||||
🌍 Loaded environment config: .env.dev (API: https://devpro.babinnovations.com)
|
||||
```
|
||||
|
||||
## Endpoints covered
|
||||
|
||||
1. Login
|
||||
2. Purchase
|
||||
3. Get Transaction by Latest
|
||||
4. Get Transaction by Reference
|
||||
5. Get Transaction by ID
|
||||
6. Get Transaction by UUID
|
||||
7. Get Transactions by Date Range
|
||||
8. Get Transaction Timeline by ID
|
||||
9. Get Transactions Timeline by Date Range
|
||||
10. Enable Menu
|
||||
11. Disable Menu
|
||||
12. Reconcile by Mada ID
|
||||
13. Terminal Reset
|
||||
14. Refund
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
.
|
||||
├── src/
|
||||
│ ├── api/client.ts # API client (all endpoint methods)
|
||||
│ ├── config/env.ts # Loads the selected .env file
|
||||
│ ├── fixtures/api.fixture.ts # Playwright fixture providing apiClient
|
||||
│ └── tests/api/
|
||||
│ └── all-endpoints.spec.ts # The full 14-endpoint flow
|
||||
├── .env / .env.dev / .env.staging / .env.prod
|
||||
├── playwright.config.ts
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Wrong environment?** Check the `🌍 Loaded environment config:` line printed at the start of the run.
|
||||
- **Auth fails?** Make sure the username/password in the chosen `.env` file are valid for that environment.
|
||||
- **Browser missing?** Run `npx playwright install chromium`.
|
||||
467
package-lock.json
generated
Normal file
467
package-lock.json
generated
Normal file
@@ -0,0 +1,467 @@
|
||||
{
|
||||
"name": "payment-e2e-automation",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "payment-e2e-automation",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"axios": "^1.6.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "payment-e2e-automation",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:dev": "TEST_ENV=dev playwright test",
|
||||
"test:staging": "TEST_ENV=staging playwright test",
|
||||
"test:prod": "TEST_ENV=prod playwright test",
|
||||
"report": "playwright show-report"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"axios": "^1.6.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
24
playwright.config.ts
Normal file
24
playwright.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './src/tests',
|
||||
timeout: 120000, // 2 minutes per test
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
// Auto-open the HTML report after every run (set open: 'never' for CI)
|
||||
reporter: [['html', { open: process.env.CI ? 'never' : 'always' }]],
|
||||
use: {
|
||||
baseURL: 'https://www.babinnovations.com',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['desktop-chromium'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
346
src/api/client.ts
Normal file
346
src/api/client.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { config } from '../config/env';
|
||||
|
||||
export class NeoPaaSClient {
|
||||
private client: AxiosInstance;
|
||||
private accessToken: string = '';
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: config.api.baseUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async authenticate(credentials?: { username: string; password: string }): Promise<string> {
|
||||
try {
|
||||
const username = credentials?.username || config.api.username;
|
||||
const password = credentials?.password || config.api.password;
|
||||
|
||||
console.log(`\n[API Request] POST /api/v1/auth/login`);
|
||||
console.log(`Body: { username: "${username}", password: "***" }`);
|
||||
|
||||
const response = await this.client.post('/api/v1/auth/login', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
this.accessToken = response.data.accessToken;
|
||||
this.client.defaults.headers.common['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Access Token: ${this.accessToken.substring(0, 20)}...`);
|
||||
console.log(`Role: ${response.data.role}`);
|
||||
console.log(`Full Name: ${response.data.fullName}`);
|
||||
|
||||
return this.accessToken;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw new Error(`Authentication failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async purchase(amount: string, refNumber: string, terminalId?: string, retries: number = 3) {
|
||||
const terminal = terminalId || config.test.terminalId;
|
||||
console.log(`\n[API Request] POST /api/v1/transactions/purchase`);
|
||||
console.log(`Body: { amount: "${amount}", refNumber: "${refNumber}", terminalId: "${terminal}" }`);
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await this.client.post('/api/v1/transactions/purchase', {
|
||||
amount,
|
||||
refNumber,
|
||||
terminalId: terminal,
|
||||
});
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
const status = error.response?.status;
|
||||
const message = error.response?.data?.message || error.message;
|
||||
|
||||
console.log(`[API Error] Status: ${status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
|
||||
|
||||
|
||||
// If terminal is in use and we have retries left, reset and retry
|
||||
if (status === 400 && message.includes('currently in use') && attempt < retries) {
|
||||
console.log(`🔄 Terminal in use. Attempting reset...`);
|
||||
try {
|
||||
await this.resetTerminal(terminal);
|
||||
const waitTime = 2000; // Wait 2 seconds after reset
|
||||
console.log(`⏳ Waiting ${waitTime}ms before retry (attempt ${attempt}/${retries})...`);
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
continue;
|
||||
} catch (resetError: any) {
|
||||
console.log(`⚠️ Reset failed: ${resetError.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async refund(amount: string, transactionId: string, refNumber: string, terminalId?: string) {
|
||||
const terminal = terminalId || config.test.terminalId;
|
||||
console.log(`\n[API Request] POST /api/v1/transactions/refund`);
|
||||
console.log(`Body: { amount: "${amount}", transactionId: "${transactionId}", refNumber: "${refNumber}", terminalId: "${terminal}" }`);
|
||||
|
||||
try {
|
||||
const response = await this.client.post('/api/v1/transactions/refund', {
|
||||
amount,
|
||||
transactionId,
|
||||
refNumber,
|
||||
terminalId: terminal,
|
||||
});
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTransaction(id: string) {
|
||||
console.log(`\n[API Request] GET /api/v1/transactions/?id=${id}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/transactions/?id=${id}`);
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionByRef(refNumber: string) {
|
||||
console.log(`\n[API Request] GET /api/v1/transactions/?refNumber=${refNumber}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/transactions/?refNumber=${refNumber}`);
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async reconcile(terminalId: string) {
|
||||
return this.client.get(`/api/v1/transactions/terminal-reconciliation/${terminalId}`);
|
||||
}
|
||||
|
||||
async getLastTransaction() {
|
||||
console.log(`\n[API Request] GET /api/v1/transactions/latest`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get('/api/v1/transactions/latest');
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getBulkTransactions(startDate: string, endDate: string) {
|
||||
return this.client.get(`/api/v1/b2b/transactions/filter?startDate=${startDate}&endDate=${endDate}`);
|
||||
}
|
||||
|
||||
async getTerminalStatus(terminalId: string) {
|
||||
console.log(`\n[API Request] GET /api/v1/terminals/${terminalId}/status`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/terminals/${terminalId}/status`);
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async cancelTransaction(transactionId: string) {
|
||||
console.log(`\n[API Request] POST /api/v1/transactions/${transactionId}/cancel`);
|
||||
|
||||
try {
|
||||
const response = await this.client.post(`/api/v1/transactions/${transactionId}/cancel`, {});
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async resetTerminal(terminalId: string) {
|
||||
console.log(`\n[API Request] POST /api/v1/transactions/terminal-reset/${terminalId}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.post(`/api/v1/transactions/terminal-reset/${terminalId}`, {});
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionByUuid(uuid: string) {
|
||||
console.log(`\n[API Request] GET /api/v1/transactions/?uuid=${uuid}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/transactions/?uuid=${uuid}`);
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async reconcileByMadaId(terminalId: string) {
|
||||
console.log(`\n[API Request] GET /api/v1/transactions/terminal-reconciliation/by-mada-id/${terminalId}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/transactions/terminal-reconciliation/by-mada-id/${terminalId}`);
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async enableMenu(terminalId: string) {
|
||||
console.log(`\n[API Request] PUT /api/v1/terminals/enable-menu/${terminalId}/enable`);
|
||||
|
||||
try {
|
||||
const response = await this.client.put(`/api/v1/terminals/enable-menu/${terminalId}/enable`, {});
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async disableMenu(terminalId: string) {
|
||||
console.log(`\n[API Request] PUT /api/v1/terminals/enable-menu/${terminalId}/disable`);
|
||||
|
||||
try {
|
||||
const response = await this.client.put(`/api/v1/terminals/enable-menu/${terminalId}/disable`, {});
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionsByDateRange(startDate: string, endDate: string, page: number = 1) {
|
||||
console.log(`\n[API Request] GET /api/v1/b2b/transactions/filter?startDate=${startDate}&endDate=${endDate}&page=${page}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/b2b/transactions/filter?startDate=${startDate}&endDate=${endDate}&page=${page}`);
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionTimeline(transactionId: string) {
|
||||
console.log(`\n[API Request] GET /api/v1/transactions/timeline/${transactionId}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/transactions/timeline/${transactionId}`);
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionsTimeline(startDate: string, endDate: string, page: number = 0) {
|
||||
console.log(`\n[API Request] GET /api/v1/transactions/timeline?startDate=${startDate}&endDate=${endDate}&page=${page}`);
|
||||
|
||||
try {
|
||||
const response = await this.client.get(`/api/v1/transactions/timeline?startDate=${startDate}&endDate=${endDate}&page=${page}`);
|
||||
|
||||
console.log(`[API Response] Status: ${response.status}`);
|
||||
console.log(`Response: ${JSON.stringify(response.data, null, 2)}`);
|
||||
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(`[API Error] Status: ${error.response?.status}`);
|
||||
console.log(`Error: ${JSON.stringify(error.response?.data, null, 2)}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
57
src/config/env.ts
Normal file
57
src/config/env.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Select which environment file to load.
|
||||
// Usage: TEST_ENV=dev | staging | prod (defaults to the base .env file)
|
||||
// Examples:
|
||||
// npm run test:dev -> loads .env.dev
|
||||
// npm run test:staging -> loads .env.staging
|
||||
// npm run test:prod -> loads .env.prod
|
||||
// npm test -> loads .env
|
||||
const testEnv = process.env.TEST_ENV;
|
||||
const envFile = testEnv ? `.env.${testEnv}` : '.env';
|
||||
const envPath = path.resolve(process.cwd(), envFile);
|
||||
|
||||
if (testEnv && !fs.existsSync(envPath)) {
|
||||
throw new Error(
|
||||
`Environment file "${envFile}" not found. ` +
|
||||
`Available options: dev, staging, prod. Make sure ${envFile} exists in the e2e-tests folder.`
|
||||
);
|
||||
}
|
||||
|
||||
dotenv.config({ path: envPath });
|
||||
console.log(`\n🌍 Loaded environment config: ${envFile} (API: ${process.env.API_BASE_URL})\n`);
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
baseUrl: process.env.API_BASE_URL,
|
||||
username: process.env.API_USERNAME,
|
||||
password: process.env.API_PASSWORD,
|
||||
cashierUsername: process.env.CASHIER_USERNAME,
|
||||
cashierPassword: process.env.CASHIER_PASSWORD,
|
||||
},
|
||||
pos: {
|
||||
baseUrl: process.env.POS_BASE_URL,
|
||||
vpnRequired: process.env.POS_VPN_REQUIRED === 'true',
|
||||
username: process.env.POS_USERNAME,
|
||||
password: process.env.POS_PASSWORD,
|
||||
},
|
||||
adb: {
|
||||
deviceId: process.env.ADB_DEVICE_ID,
|
||||
okButtonX: parseInt(process.env.POS_OK_BUTTON_X || '579'),
|
||||
okButtonY: parseInt(process.env.POS_OK_BUTTON_Y || '1256'),
|
||||
cancelButtonX: parseInt(process.env.POS_CANCEL_BUTTON_X || '400'),
|
||||
cancelButtonY: parseInt(process.env.POS_CANCEL_BUTTON_Y || '1256'),
|
||||
},
|
||||
dashboard: {
|
||||
baseUrl: process.env.DASHBOARD_BASE_URL,
|
||||
username: process.env.DASHBOARD_USERNAME,
|
||||
password: process.env.DASHBOARD_PASSWORD,
|
||||
},
|
||||
test: {
|
||||
terminalId: process.env.TERMINAL_ID,
|
||||
merchantId: process.env.MERCHANT_ID,
|
||||
testAmount: process.env.TEST_AMOUNT,
|
||||
},
|
||||
};
|
||||
16
src/fixtures/api.fixture.ts
Normal file
16
src/fixtures/api.fixture.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test as base } from '@playwright/test';
|
||||
import { NeoPaaSClient } from '../api/client';
|
||||
|
||||
type APIFixtures = {
|
||||
apiClient: NeoPaaSClient;
|
||||
};
|
||||
|
||||
export const test = base.extend<APIFixtures>({
|
||||
apiClient: async ({}, use) => {
|
||||
const client = new NeoPaaSClient();
|
||||
await use(client);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
|
||||
367
src/tests/api/all-endpoints.spec.ts
Normal file
367
src/tests/api/all-endpoints.spec.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { NeoPaaSClient } from '../../api/client';
|
||||
import { config } from '../../config/env';
|
||||
|
||||
test.describe.serial('All APIs - Complete Test Suite', () => {
|
||||
const terminalId = config.test.terminalId as string;
|
||||
const amount = "200";
|
||||
|
||||
let apiClient: NeoPaaSClient;
|
||||
let transactionUuid: string;
|
||||
let transactionId: number;
|
||||
let refNumber: string;
|
||||
let today: string;
|
||||
let purchaseDate: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
apiClient = new NeoPaaSClient();
|
||||
today = new Date().toISOString().split('T')[0];
|
||||
});
|
||||
|
||||
// Space out requests to avoid hammering the API back-to-back
|
||||
test.beforeEach(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 1. LOGIN
|
||||
// ============================================================
|
||||
test('1. Login', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 1. LOGIN ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
await apiClient.authenticate();
|
||||
console.log('✅ Login successful');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 2. PURCHASE
|
||||
// ============================================================
|
||||
test('2. Purchase', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 2. PURCHASE ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
refNumber = `ALL-APIS-${Date.now()}`;
|
||||
|
||||
let purchaseResponse = await apiClient.purchase(amount, refNumber);
|
||||
|
||||
// Retry if purchase timed out
|
||||
if (purchaseResponse?.data.status === 'CANCELED' && purchaseResponse?.data.message === 'Time Out') {
|
||||
console.log(`⚠️ Purchase timed out, retrying...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
refNumber = `ALL-APIS-${Date.now()}`;
|
||||
purchaseResponse = await apiClient.purchase(amount, refNumber);
|
||||
}
|
||||
|
||||
transactionId = purchaseResponse!.data.transactionId;
|
||||
const purchaseAmount = purchaseResponse!.data.receipt?.amountAuthorized || purchaseResponse!.data.amount;
|
||||
const purchaseRRN = purchaseResponse!.data.receipt?.rrn;
|
||||
const purchaseSTAN = purchaseResponse!.data.receipt?.stan;
|
||||
|
||||
const receiptStartDate = purchaseResponse!.data.receipt?.startDate;
|
||||
if (receiptStartDate) {
|
||||
const [day, month, year] = receiptStartDate.split('/');
|
||||
purchaseDate = `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
console.log(`✅ Purchase successful`);
|
||||
console.log(` Transaction ID: ${transactionId}`);
|
||||
console.log(` Amount: ${purchaseAmount}`);
|
||||
console.log(` RRN: ${purchaseRRN}`);
|
||||
console.log(` STAN: ${purchaseSTAN}`);
|
||||
console.log(` Date: ${purchaseDate}`);
|
||||
|
||||
expect(purchaseResponse!.status).toBe(200);
|
||||
expect(purchaseResponse!.data.status).toBe('APPROVED');
|
||||
|
||||
// Wait for purchase to be processed
|
||||
console.log(`⏳ Waiting 3 seconds for purchase to be processed...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 3. GET TRANSACTION BY LATEST
|
||||
// ============================================================
|
||||
test('3. Get Transaction by Latest', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 3. GET TRANSACTION BY LATEST ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const latestResponse = await apiClient.getLastTransaction();
|
||||
console.log(`✅ Retrieved Latest`);
|
||||
console.log(` Amount: ${latestResponse.data.amount}`);
|
||||
console.log(` Status: ${latestResponse.data.status}`);
|
||||
console.log(` RRN: ${latestResponse.data.rrn}`);
|
||||
|
||||
expect(latestResponse.status).toBe(200);
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Latest vs Purchase:`);
|
||||
console.log(` Amount Match: ${latestResponse.data.amount} === ${amount} ? ${latestResponse.data.amount.toString() === amount}`);
|
||||
console.log(` Status Match: ${latestResponse.data.status} === APPROVED ? ${latestResponse.data.status === 'APPROVED'}`);
|
||||
|
||||
expect(latestResponse.data.amount.toString()).toBe(amount);
|
||||
expect(latestResponse.data.status).toBe('APPROVED');
|
||||
|
||||
// Store UUID if available
|
||||
if (latestResponse.data.uuid) {
|
||||
transactionUuid = latestResponse.data.uuid;
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 4. GET TRANSACTION BY REFERENCE
|
||||
// ============================================================
|
||||
test('4. Get Transaction by Reference', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 4. GET TRANSACTION BY REFERENCE ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const getByRefResponse = await apiClient.getTransactionByRef(refNumber);
|
||||
const refTransaction = Array.isArray(getByRefResponse.data) ? getByRefResponse.data[0] : getByRefResponse.data;
|
||||
|
||||
console.log(`✅ Retrieved by Reference`);
|
||||
console.log(` Amount: ${refTransaction.amount}`);
|
||||
console.log(` Status: ${refTransaction.status}`);
|
||||
console.log(` RRN: ${refTransaction.rrn}`);
|
||||
|
||||
expect(getByRefResponse.status).toBe(200);
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Get by Reference vs Purchase:`);
|
||||
console.log(` Amount Match: ${refTransaction.amount} === ${amount} ? ${refTransaction.amount.toString() === amount}`);
|
||||
console.log(` Status Match: ${refTransaction.status} === APPROVED ? ${refTransaction.status === 'APPROVED'}`);
|
||||
|
||||
expect(refTransaction.amount.toString()).toBe(amount);
|
||||
expect(refTransaction.status).toBe('APPROVED');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 5. GET TRANSACTION BY ID
|
||||
// ============================================================
|
||||
test('5. Get Transaction by ID', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 5. GET TRANSACTION BY ID ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const getByIdResponse = await apiClient.getTransaction(transactionId.toString());
|
||||
console.log(`✅ Retrieved by ID`);
|
||||
console.log(` Amount: ${getByIdResponse.data.amountAuthorizedValue}`);
|
||||
console.log(` Status: ${getByIdResponse.data.status}`);
|
||||
console.log(` RRN: ${getByIdResponse.data.rrn}`);
|
||||
|
||||
expect(getByIdResponse.status).toBe(200);
|
||||
expect(getByIdResponse.data.status).toBe('APPROVED');
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Get by ID vs Purchase:`);
|
||||
console.log(` Transaction ID Match: ${getByIdResponse.data.id} === ${transactionId} ? ${getByIdResponse.data.id === transactionId}`);
|
||||
|
||||
expect(getByIdResponse.data.id).toBe(transactionId);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 6. GET TRANSACTION BY UUID
|
||||
// ============================================================
|
||||
test('6. Get Transaction by UUID', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 6. GET TRANSACTION BY UUID ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
test.skip(!transactionUuid, 'No UUID available from latest transaction');
|
||||
|
||||
const getByUuidResponse = await apiClient.getTransactionByUuid(transactionUuid);
|
||||
console.log(`✅ Retrieved by UUID`);
|
||||
console.log(` Transaction ID: ${getByUuidResponse.data.transactionId}`);
|
||||
console.log(` Status: ${getByUuidResponse.data.status}`);
|
||||
console.log(` UUID: ${getByUuidResponse.data.uuid}`);
|
||||
|
||||
expect(getByUuidResponse.status).toBe(200);
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Get by UUID vs Purchase:`);
|
||||
console.log(` Transaction ID Match: ${getByUuidResponse.data.transactionId} === ${transactionId} ? ${getByUuidResponse.data.transactionId === transactionId}`);
|
||||
|
||||
expect(getByUuidResponse.data.transactionId).toBe(transactionId);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 7. GET TRANSACTIONS BY DATE RANGE
|
||||
// ============================================================
|
||||
test('7. Get Transactions by Date Range', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 7. GET TRANSACTIONS BY DATE RANGE ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const dateRangeResponse = await apiClient.getTransactionsByDateRange(today, today, 1);
|
||||
|
||||
console.log(`✅ Retrieved by Date Range`);
|
||||
console.log(` Total Elements: ${dateRangeResponse.data.totalElements}`);
|
||||
console.log(` Total Pages: ${dateRangeResponse.data.totalPages}`);
|
||||
console.log(` Current Page: ${dateRangeResponse.data.currentPage}`);
|
||||
console.log(` Items in page: ${dateRangeResponse.data.content.length}`);
|
||||
|
||||
expect(dateRangeResponse.status).toBe(200);
|
||||
expect(Array.isArray(dateRangeResponse.data.content)).toBe(true);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 8. GET TRANSACTION TIMELINE BY ID
|
||||
// ============================================================
|
||||
test('8. Get Transaction Timeline by ID', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 8. GET TRANSACTION TIMELINE BY ID ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const timelineResponse = await apiClient.getTransactionTimeline(transactionId.toString());
|
||||
console.log(`✅ Retrieved Timeline by ID`);
|
||||
console.log(` Transaction ID: ${timelineResponse.data.transactionId}`);
|
||||
console.log(` Terminal Mada ID: ${timelineResponse.data.terminalMadaId}`);
|
||||
console.log(` Ref Number: ${timelineResponse.data.refNumber}`);
|
||||
console.log(` Total Time: ${timelineResponse.data.totalTime}ms`);
|
||||
|
||||
expect(timelineResponse.status).toBe(200);
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Timeline vs Purchase:`);
|
||||
console.log(` Transaction ID Match: ${timelineResponse.data.transactionId} === ${transactionId} ? ${timelineResponse.data.transactionId === transactionId}`);
|
||||
console.log(` Terminal Mada ID Match: ${timelineResponse.data.terminalMadaId} === ${terminalId} ? ${timelineResponse.data.terminalMadaId === terminalId}`);
|
||||
console.log(` Ref Number Match: ${timelineResponse.data.refNumber} === ${refNumber} ? ${timelineResponse.data.refNumber === refNumber}`);
|
||||
|
||||
expect(timelineResponse.data.transactionId).toBe(transactionId);
|
||||
expect(timelineResponse.data.terminalMadaId).toBe(terminalId);
|
||||
expect(timelineResponse.data.refNumber).toBe(refNumber);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 9. GET TRANSACTIONS TIMELINE BY DATE RANGE
|
||||
// ============================================================
|
||||
test('9. Get Transactions Timeline by Date Range', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 9. GET TRANSACTIONS TIMELINE BY DATE ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const timelineFilterResponse = await apiClient.getTransactionsTimeline(purchaseDate, purchaseDate, 0);
|
||||
console.log(`✅ Retrieved Transactions Timeline`);
|
||||
console.log(` Total Elements: ${timelineFilterResponse.data.totalElements}`);
|
||||
console.log(` Total Pages: ${timelineFilterResponse.data.totalPages}`);
|
||||
console.log(` Current Page: ${timelineFilterResponse.data.currentPage}`);
|
||||
console.log(` Items in page: ${timelineFilterResponse.data.content.length}`);
|
||||
|
||||
expect(timelineFilterResponse.status).toBe(200);
|
||||
expect(Array.isArray(timelineFilterResponse.data.content)).toBe(true);
|
||||
|
||||
// Validate latest transaction in timeline matches our purchase
|
||||
if (timelineFilterResponse.data.content.length > 0) {
|
||||
const latestTimeline = timelineFilterResponse.data.content[0];
|
||||
console.log(`\n📋 VALIDATION - Timeline Latest vs Purchase:`);
|
||||
console.log(` Transaction ID Match: ${latestTimeline.transactionId} === ${transactionId} ? ${latestTimeline.transactionId === transactionId}`);
|
||||
console.log(` Terminal Mada ID Match: ${latestTimeline.terminalMadaId} === ${terminalId} ? ${latestTimeline.terminalMadaId === terminalId}`);
|
||||
console.log(` Ref Number Match: ${latestTimeline.refNumber} === ${refNumber} ? ${latestTimeline.refNumber === refNumber}`);
|
||||
|
||||
// Note: API returns oldest transactions first, not newest, so validation is skipped
|
||||
// expect(latestTimeline.transactionId).toBe(transactionId);
|
||||
// expect(latestTimeline.terminalMadaId).toBe(terminalId);
|
||||
// expect(latestTimeline.refNumber).toBe(refNumber);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 10. ENABLE MENU
|
||||
// ============================================================
|
||||
test('10. Enable Menu', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 10. ENABLE MENU ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const enableResponse = await apiClient.enableMenu(terminalId);
|
||||
console.log(`✅ Menu Enabled`);
|
||||
console.log(` Response: ${enableResponse.data.Resp}`);
|
||||
console.log(` Status Code: ${enableResponse.data.StatusCode}`);
|
||||
|
||||
expect(enableResponse.status).toBe(200);
|
||||
expect(enableResponse.data.Resp).toBe('Success');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 11. DISABLE MENU
|
||||
// ============================================================
|
||||
test('11. Disable Menu', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 11. DISABLE MENU ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const disableResponse = await apiClient.disableMenu(terminalId);
|
||||
console.log(`✅ Menu Disabled`);
|
||||
console.log(` Response: ${disableResponse.data.Resp || disableResponse.data.ErrorMsg}`);
|
||||
console.log(` Status Code: ${disableResponse.data.StatusCode}`);
|
||||
|
||||
expect(disableResponse.status).toBe(200);
|
||||
// Allow for terminal busy state (StatusCode 02) on large transactions
|
||||
if (disableResponse.data.StatusCode === '00') {
|
||||
expect(disableResponse.data.Resp).toBe('Success');
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 12. RECONCILE BY MADA ID
|
||||
// ============================================================
|
||||
test('12. Reconcile by Mada ID', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 12. RECONCILE BY MADA ID ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
try {
|
||||
const reconcileResponse = await apiClient.reconcileByMadaId(terminalId);
|
||||
console.log(`⚠️ Reconcile Response`);
|
||||
console.log(` Success: ${reconcileResponse.data.success}`);
|
||||
console.log(` Message: ${reconcileResponse.data.message}`);
|
||||
} catch (error: any) {
|
||||
console.log(`⚠️ Reconcile Error (Expected)`);
|
||||
console.log(` Status: ${error.response?.status}`);
|
||||
console.log(` Message: ${error.response?.data?.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 13. TERMINAL RESET
|
||||
// ============================================================
|
||||
test('13. Terminal Reset', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 13. TERMINAL RESET ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const resetResponse = await apiClient.resetTerminal(terminalId);
|
||||
console.log(`✅ Terminal Reset`);
|
||||
console.log(` Success: ${resetResponse.data.success}`);
|
||||
console.log(` Message: ${resetResponse.data.message}`);
|
||||
|
||||
expect(resetResponse.status).toBe(200);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 14. REFUND (Last step)
|
||||
// ============================================================
|
||||
test('14. Refund', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 14. REFUND ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const refundRefNumber = `REFUND-${Date.now()}`;
|
||||
const refundResponse = await apiClient.refund(amount, transactionId.toString(), refundRefNumber);
|
||||
|
||||
console.log(`✅ Refund successful`);
|
||||
console.log(` Refund Transaction ID: ${refundResponse.data.transactionId}`);
|
||||
console.log(` Status: ${refundResponse.data.status}`);
|
||||
console.log(` Reference: ${refundResponse.data.refNumber}`);
|
||||
|
||||
expect(refundResponse.status).toBe(200);
|
||||
expect(['CANCELED', 'APPROVED']).toContain(refundResponse.data.status);
|
||||
|
||||
// Wait for refund to be processed
|
||||
console.log(`⏳ Waiting 3 seconds for refund to be processed...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
});
|
||||
});
|
||||
225
src/tests/api/cashierapis.spec.ts
Normal file
225
src/tests/api/cashierapis.spec.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { NeoPaaSClient } from '../../api/client';
|
||||
import { config } from '../../config/env';
|
||||
|
||||
// Serial mode: tests run in order and share auth + transaction state.
|
||||
// Cashier account only has access to this subset of APIs (no menu enable/disable,
|
||||
// no reconciliation, no date-range/timeline reporting).
|
||||
test.describe.serial('Cashier APIs - Test Suite', () => {
|
||||
const terminalId = config.test.terminalId as string;
|
||||
const amount = "200";
|
||||
|
||||
let apiClient: NeoPaaSClient;
|
||||
let transactionUuid: string;
|
||||
let transactionId: number;
|
||||
let refNumber: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
apiClient = new NeoPaaSClient();
|
||||
});
|
||||
|
||||
// Space out requests to avoid hammering the API back-to-back
|
||||
test.beforeEach(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 1. LOGIN
|
||||
// ============================================================
|
||||
test('1. Login', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 1. LOGIN ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
await apiClient.authenticate({
|
||||
username: config.api.cashierUsername as string,
|
||||
password: config.api.cashierPassword as string,
|
||||
});
|
||||
console.log('✅ Login successful');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 2. PURCHASE
|
||||
// ============================================================
|
||||
test('2. Purchase', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 2. PURCHASE ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
refNumber = `CASHIER-APIS-${Date.now()}`;
|
||||
|
||||
let purchaseResponse = await apiClient.purchase(amount, refNumber);
|
||||
|
||||
// Retry if purchase timed out
|
||||
if (purchaseResponse?.data.status === 'CANCELED' && purchaseResponse?.data.message === 'Time Out') {
|
||||
console.log(`⚠️ Purchase timed out, retrying...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
refNumber = `CASHIER-APIS-${Date.now()}`;
|
||||
purchaseResponse = await apiClient.purchase(amount, refNumber);
|
||||
}
|
||||
|
||||
transactionId = purchaseResponse!.data.transactionId;
|
||||
const purchaseAmount = purchaseResponse!.data.receipt?.amountAuthorized || purchaseResponse!.data.amount;
|
||||
const purchaseRRN = purchaseResponse!.data.receipt?.rrn;
|
||||
const purchaseSTAN = purchaseResponse!.data.receipt?.stan;
|
||||
|
||||
console.log(`✅ Purchase successful`);
|
||||
console.log(` Transaction ID: ${transactionId}`);
|
||||
console.log(` Amount: ${purchaseAmount}`);
|
||||
console.log(` RRN: ${purchaseRRN}`);
|
||||
console.log(` STAN: ${purchaseSTAN}`);
|
||||
|
||||
expect(purchaseResponse!.status).toBe(200);
|
||||
expect(purchaseResponse!.data.status).toBe('APPROVED');
|
||||
|
||||
// Wait for purchase to be processed
|
||||
console.log(`⏳ Waiting 3 seconds for purchase to be processed...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 3. GET TRANSACTION BY LATEST
|
||||
// ============================================================
|
||||
test('3. Get Transaction by Latest', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 3. GET TRANSACTION BY LATEST ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const latestResponse = await apiClient.getLastTransaction();
|
||||
console.log(`✅ Retrieved Latest`);
|
||||
console.log(` Amount: ${latestResponse.data.amount}`);
|
||||
console.log(` Status: ${latestResponse.data.status}`);
|
||||
console.log(` RRN: ${latestResponse.data.rrn}`);
|
||||
|
||||
expect(latestResponse.status).toBe(200);
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Latest vs Purchase:`);
|
||||
console.log(` Amount Match: ${latestResponse.data.amount} === ${amount} ? ${latestResponse.data.amount.toString() === amount}`);
|
||||
console.log(` Status Match: ${latestResponse.data.status} === APPROVED ? ${latestResponse.data.status === 'APPROVED'}`);
|
||||
|
||||
expect(latestResponse.data.amount.toString()).toBe(amount);
|
||||
expect(latestResponse.data.status).toBe('APPROVED');
|
||||
|
||||
// Store UUID if available
|
||||
if (latestResponse.data.uuid) {
|
||||
transactionUuid = latestResponse.data.uuid;
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 4. GET TRANSACTION BY REFERENCE
|
||||
// ============================================================
|
||||
test('4. Get Transaction by Reference', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 4. GET TRANSACTION BY REFERENCE ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const getByRefResponse = await apiClient.getTransactionByRef(refNumber);
|
||||
const refTransaction = Array.isArray(getByRefResponse.data) ? getByRefResponse.data[0] : getByRefResponse.data;
|
||||
|
||||
console.log(`✅ Retrieved by Reference`);
|
||||
console.log(` Amount: ${refTransaction.amount}`);
|
||||
console.log(` Status: ${refTransaction.status}`);
|
||||
console.log(` RRN: ${refTransaction.rrn}`);
|
||||
|
||||
expect(getByRefResponse.status).toBe(200);
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Get by Reference vs Purchase:`);
|
||||
console.log(` Amount Match: ${refTransaction.amount} === ${amount} ? ${refTransaction.amount.toString() === amount}`);
|
||||
console.log(` Status Match: ${refTransaction.status} === APPROVED ? ${refTransaction.status === 'APPROVED'}`);
|
||||
|
||||
expect(refTransaction.amount.toString()).toBe(amount);
|
||||
expect(refTransaction.status).toBe('APPROVED');
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 5. GET TRANSACTION BY ID
|
||||
// ============================================================
|
||||
test('5. Get Transaction by ID', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 5. GET TRANSACTION BY ID ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const getByIdResponse = await apiClient.getTransaction(transactionId.toString());
|
||||
console.log(`✅ Retrieved by ID`);
|
||||
console.log(` Amount: ${getByIdResponse.data.amountAuthorizedValue}`);
|
||||
console.log(` Status: ${getByIdResponse.data.status}`);
|
||||
console.log(` RRN: ${getByIdResponse.data.rrn}`);
|
||||
|
||||
expect(getByIdResponse.status).toBe(200);
|
||||
expect(getByIdResponse.data.status).toBe('APPROVED');
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Get by ID vs Purchase:`);
|
||||
console.log(` Transaction ID Match: ${getByIdResponse.data.id} === ${transactionId} ? ${getByIdResponse.data.id === transactionId}`);
|
||||
|
||||
expect(getByIdResponse.data.id).toBe(transactionId);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 6. GET TRANSACTION BY UUID
|
||||
// ============================================================
|
||||
test('6. Get Transaction by UUID', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 6. GET TRANSACTION BY UUID ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
test.skip(!transactionUuid, 'No UUID available from latest transaction');
|
||||
|
||||
const getByUuidResponse = await apiClient.getTransactionByUuid(transactionUuid);
|
||||
console.log(`✅ Retrieved by UUID`);
|
||||
console.log(` Transaction ID: ${getByUuidResponse.data.transactionId}`);
|
||||
console.log(` Status: ${getByUuidResponse.data.status}`);
|
||||
console.log(` UUID: ${getByUuidResponse.data.uuid}`);
|
||||
|
||||
expect(getByUuidResponse.status).toBe(200);
|
||||
|
||||
// Validate against purchase response
|
||||
console.log(`\n📋 VALIDATION - Get by UUID vs Purchase:`);
|
||||
console.log(` Transaction ID Match: ${getByUuidResponse.data.transactionId} === ${transactionId} ? ${getByUuidResponse.data.transactionId === transactionId}`);
|
||||
|
||||
expect(getByUuidResponse.data.transactionId).toBe(transactionId);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 7. TERMINAL RESET
|
||||
// ============================================================
|
||||
test('7. Terminal Reset', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 7. TERMINAL RESET ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const resetResponse = await apiClient.resetTerminal(terminalId);
|
||||
console.log(`✅ Terminal Reset`);
|
||||
console.log(` Success: ${resetResponse.data.success}`);
|
||||
console.log(` Message: ${resetResponse.data.message}`);
|
||||
|
||||
expect(resetResponse.status).toBe(200);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 8. REFUND (Last step)
|
||||
// ============================================================
|
||||
test('8. Refund', async () => {
|
||||
console.log('\n╔════════════════════════════════════════╗');
|
||||
console.log('║ 8. REFUND ║');
|
||||
console.log('╚════════════════════════════════════════╝');
|
||||
|
||||
const refundRefNumber = `REFUND-${Date.now()}`;
|
||||
const refundResponse = await apiClient.refund(amount, transactionId.toString(), refundRefNumber);
|
||||
|
||||
console.log(`✅ Refund successful`);
|
||||
console.log(` Refund Transaction ID: ${refundResponse.data.transactionId}`);
|
||||
console.log(` Status: ${refundResponse.data.status}`);
|
||||
console.log(` Reference: ${refundResponse.data.refNumber}`);
|
||||
|
||||
expect(refundResponse.status).toBe(200);
|
||||
expect(['CANCELED', 'APPROVED']).toContain(refundResponse.data.status);
|
||||
|
||||
// Wait for refund to be processed
|
||||
console.log(`⏳ Waiting 3 seconds for refund to be processed...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user