From 9d5c7a477dc9cac8cc8664cfecf716727a411b20 Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 15:35:06 +0100 Subject: [PATCH 1/9] chore: updated GitHub Action file --- .github/workflows/postman.yaml | 13 +++--------- .postman/resources.yaml | 9 ++++++++ .../.resources/definition.yaml | 21 +++++++++++++++++++ .../Accounts/Create Account.request.yaml | 17 +++++++++++++++ .../Accounts/Delete Account.request.yaml | 5 +++++ .../Accounts/Get Account.request.yaml | 5 +++++ .../Accounts/List Accounts.request.yaml | 14 +++++++++++++ .../Accounts/Update Account.request.yaml | 16 ++++++++++++++ .../Auth/Generate API Key.request.yaml | 7 +++++++ .../General/Health Check.request.yaml | 7 +++++++ .../General/Welcome.request.yaml | 7 +++++++ .../Create Transaction.request.yaml | 17 +++++++++++++++ .../Transactions/Get Transaction.request.yaml | 5 +++++ .../List Transactions.request.yaml | 16 ++++++++++++++ postman/globals/workspace.globals.yaml | 2 ++ 15 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 .postman/resources.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/.resources/definition.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Accounts/Create Account.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Accounts/Delete Account.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Accounts/Get Account.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Accounts/List Accounts.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Accounts/Update Account.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Auth/Generate API Key.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/General/Health Check.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/General/Welcome.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Transactions/Create Transaction.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Transactions/Get Transaction.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Transactions/List Transactions.request.yaml create mode 100644 postman/globals/workspace.globals.yaml diff --git a/.github/workflows/postman.yaml b/.github/workflows/postman.yaml index 7316f6f..a13b29e 100644 --- a/.github/workflows/postman.yaml +++ b/.github/workflows/postman.yaml @@ -13,20 +13,13 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "yarn" - - run: curl -o- "https://dl-cli.pstmn.io/install/unix.sh" | sh - - name: Validate and sync + - name: Validate Collection and Spec env: POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} run: | postman login --with-api-key "$POSTMAN_API_KEY" - yarn install --frozen-lockfile - yarn start & - sleep 3 - yarn test:api + postman collection run /path/to/collection.json + postman workspace prepare postman workspace push -y diff --git a/.postman/resources.yaml b/.postman/resources.yaml new file mode 100644 index 0000000..c571af2 --- /dev/null +++ b/.postman/resources.yaml @@ -0,0 +1,9 @@ +# Use this workspace to collaborate +workspace: + id: 6c050d74-b1e6-4dfb-943e-2f7a2565e4e5 + +# All resources in the `postman/` folder are automatically registered in Local View. +# Point to additional files outside the `postman/` folder to register them individually. Example: +#localResources: +# collections: +# - ../tests/E2E Test Collection/ diff --git a/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml b/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml new file mode 100644 index 0000000..79893c4 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml @@ -0,0 +1,21 @@ +$kind: collection +name: Intergalactic Bank API +description: "A banking API for the Intergalactic Bank. Supports account management and transactions. Auth via `x-api-key` header." +auth: + type: apikey + credentials: + - key: key + value: x-api-key + - key: value + value: '{{apiKey}}' + - key: in + value: header +variables: + - key: baseUrl + value: 'http://localhost:3000' + - key: apiKey + value: '1234' + - key: accountId + value: '1' + - key: transactionId + value: '1' diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/Create Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Accounts/Create Account.request.yaml new file mode 100644 index 0000000..9aff407 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Accounts/Create Account.request.yaml @@ -0,0 +1,17 @@ +$kind: http-request +name: Create Account +method: POST +url: '{{baseUrl}}/api/v1/accounts' +order: 2000 +headers: + - key: Content-Type + value: application/json +body: + type: json + content: |- + { + "owner": "Nova Newman", + "currency": "COSMIC_COINS", + "balance": 0, + "accountType": "STANDARD" + } diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/Delete Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Accounts/Delete Account.request.yaml new file mode 100644 index 0000000..8b8a366 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Accounts/Delete Account.request.yaml @@ -0,0 +1,5 @@ +$kind: http-request +name: Delete Account +method: DELETE +url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' +order: 5000 diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/Get Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Accounts/Get Account.request.yaml new file mode 100644 index 0000000..a495d1b --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Accounts/Get Account.request.yaml @@ -0,0 +1,5 @@ +$kind: http-request +name: Get Account +method: GET +url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' +order: 3000 diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/List Accounts.request.yaml b/postman/collections/Intergalactic-Bank-API/Accounts/List Accounts.request.yaml new file mode 100644 index 0000000..00b1b36 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Accounts/List Accounts.request.yaml @@ -0,0 +1,14 @@ +$kind: http-request +name: List Accounts +method: GET +url: '{{baseUrl}}/api/v1/accounts' +order: 1000 +queryParams: + - key: owner + value: '' + description: Partial match filter on owner name + disabled: true + - key: createdAt + value: '' + description: 'Exact date match (YYYY-MM-DD)' + disabled: true diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/Update Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Accounts/Update Account.request.yaml new file mode 100644 index 0000000..de909e4 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Accounts/Update Account.request.yaml @@ -0,0 +1,16 @@ +$kind: http-request +name: Update Account +method: PUT +url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' +order: 4000 +headers: + - key: Content-Type + value: application/json +body: + type: json + content: |- + { + "owner": "Updated Name", + "accountType": "PREMIUM", + "currency": "COSMIC_COINS" + } diff --git a/postman/collections/Intergalactic-Bank-API/Auth/Generate API Key.request.yaml b/postman/collections/Intergalactic-Bank-API/Auth/Generate API Key.request.yaml new file mode 100644 index 0000000..3301601 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Auth/Generate API Key.request.yaml @@ -0,0 +1,7 @@ +$kind: http-request +name: Generate API Key +method: GET +url: '{{baseUrl}}/api/v1/auth' +order: 1000 +auth: + type: noauth diff --git a/postman/collections/Intergalactic-Bank-API/General/Health Check.request.yaml b/postman/collections/Intergalactic-Bank-API/General/Health Check.request.yaml new file mode 100644 index 0000000..45123e7 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/General/Health Check.request.yaml @@ -0,0 +1,7 @@ +$kind: http-request +name: Health Check +method: GET +url: '{{baseUrl}}/health' +order: 2000 +auth: + type: noauth diff --git a/postman/collections/Intergalactic-Bank-API/General/Welcome.request.yaml b/postman/collections/Intergalactic-Bank-API/General/Welcome.request.yaml new file mode 100644 index 0000000..c93a25f --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/General/Welcome.request.yaml @@ -0,0 +1,7 @@ +$kind: http-request +name: Welcome +method: GET +url: '{{baseUrl}}/' +order: 1000 +auth: + type: noauth diff --git a/postman/collections/Intergalactic-Bank-API/Transactions/Create Transaction.request.yaml b/postman/collections/Intergalactic-Bank-API/Transactions/Create Transaction.request.yaml new file mode 100644 index 0000000..6ee199f --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Transactions/Create Transaction.request.yaml @@ -0,0 +1,17 @@ +$kind: http-request +name: Create Transaction +method: POST +url: '{{baseUrl}}/api/v1/transactions' +order: 2000 +headers: + - key: Content-Type + value: application/json +body: + type: json + content: |- + { + "fromAccountId": "1", + "toAccountId": "2", + "amount": 100, + "currency": "COSMIC_COINS" + } diff --git a/postman/collections/Intergalactic-Bank-API/Transactions/Get Transaction.request.yaml b/postman/collections/Intergalactic-Bank-API/Transactions/Get Transaction.request.yaml new file mode 100644 index 0000000..977f817 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Transactions/Get Transaction.request.yaml @@ -0,0 +1,5 @@ +$kind: http-request +name: Get Transaction +method: GET +url: '{{baseUrl}}/api/v1/transactions/{{transactionId}}' +order: 3000 diff --git a/postman/collections/Intergalactic-Bank-API/Transactions/List Transactions.request.yaml b/postman/collections/Intergalactic-Bank-API/Transactions/List Transactions.request.yaml new file mode 100644 index 0000000..e850015 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Transactions/List Transactions.request.yaml @@ -0,0 +1,16 @@ +$kind: http-request +name: List Transactions +method: GET +url: '{{baseUrl}}/api/v1/transactions' +order: 1000 +queryParams: + - key: fromAccountId + value: '' + disabled: true + - key: toAccountId + value: '' + disabled: true + - key: createdAt + value: '' + description: 'Exact date match (YYYY-MM-DD)' + disabled: true diff --git a/postman/globals/workspace.globals.yaml b/postman/globals/workspace.globals.yaml new file mode 100644 index 0000000..e96c6d6 --- /dev/null +++ b/postman/globals/workspace.globals.yaml @@ -0,0 +1,2 @@ +name: Globals +values: [] From 771c17a52f1044c5d6aceac942b78cc6bcef898b Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 15:39:52 +0100 Subject: [PATCH 2/9] fix: updated collection path --- .github/workflows/postman.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/postman.yaml b/.github/workflows/postman.yaml index a13b29e..1221b83 100644 --- a/.github/workflows/postman.yaml +++ b/.github/workflows/postman.yaml @@ -20,6 +20,6 @@ jobs: POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} run: | postman login --with-api-key "$POSTMAN_API_KEY" - postman collection run /path/to/collection.json + postman collection run ./postman/collections/Intergalactic-Bank-API postman workspace prepare postman workspace push -y From ad7b8201336a2b8f0b73701bc5e66dacbabac28a Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 15:48:02 +0100 Subject: [PATCH 3/9] fix: ensure server is running before running tests --- .github/workflows/postman.yaml | 30 +++++++++++++++++++ .../.resources/definition.yaml | 24 ++++++--------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/.github/workflows/postman.yaml b/.github/workflows/postman.yaml index 1221b83..cf63b15 100644 --- a/.github/workflows/postman.yaml +++ b/.github/workflows/postman.yaml @@ -15,6 +15,28 @@ jobs: - run: curl -o- "https://dl-cli.pstmn.io/install/unix.sh" | sh + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Start server in background + run: | + npm run dev & + echo $! > pidfile + # Wait for server to be ready (port 3000) + for i in {1..20}; do + if nc -z localhost 3000; then + echo "Server is up!" + break + fi + echo "Waiting for server..." + sleep 2 + done + - name: Validate Collection and Spec env: POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} @@ -23,3 +45,11 @@ jobs: postman collection run ./postman/collections/Intergalactic-Bank-API postman workspace prepare postman workspace push -y + + - name: Stop server + if: always() + run: | + if [ -f pidfile ]; then + kill $(cat pidfile) || true + rm pidfile + fi diff --git a/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml b/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml index 79893c4..97db2e6 100644 --- a/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml +++ b/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml @@ -1,21 +1,15 @@ $kind: collection name: Intergalactic Bank API -description: "A banking API for the Intergalactic Bank. Supports account management and transactions. Auth via `x-api-key` header." +description: A banking API for the Intergalactic Bank. Supports account + management and transactions. Auth via `x-api-key` header. auth: type: apikey credentials: - - key: key - value: x-api-key - - key: value - value: '{{apiKey}}' - - key: in - value: header + key: x-api-key + value: "{{apiKey}}" + in: header variables: - - key: baseUrl - value: 'http://localhost:3000' - - key: apiKey - value: '1234' - - key: accountId - value: '1' - - key: transactionId - value: '1' + baseUrl: https://template.postman-echo.com + apiKey: "1234" + accountId: "1" + transactionId: "1" From 55ccd9a85aeb159d6af2b28285eb93e503424044 Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 15:52:12 +0100 Subject: [PATCH 4/9] fix: ensure server is running before running tests --- .github/workflows/postman.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/postman.yaml b/.github/workflows/postman.yaml index cf63b15..0062f08 100644 --- a/.github/workflows/postman.yaml +++ b/.github/workflows/postman.yaml @@ -19,9 +19,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 + cache: "npm" - name: Install dependencies - run: npm ci + run: npm install - name: Start server in background run: | From e024839f6ec6c85258b6923cab86ff90400e2b5a Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 16:09:19 +0100 Subject: [PATCH 5/9] chore: updated collection --- .../.resources/definition.yaml | 26 ++++++++++++------- .../Accounts/Create Account.request.yaml | 17 ------------ .../Auth/Generate API Key.request.yaml | 7 ----- .../Create-Account-2.request.yaml | 24 +++++++++++++++++ .../Create-Account.request.yaml | 24 +++++++++++++++++ .../Create-Transaction.request.yaml | 24 +++++++++++++++++ .../Delete-Account-2.request.yaml | 5 ++++ ...quest.yaml => Delete-Account.request.yaml} | 2 +- .../Generate-API-Key.request.yaml | 14 ++++++++++ ....request.yaml => Get-Account.request.yaml} | 2 +- ...uest.yaml => Get-Transaction.request.yaml} | 2 +- ...request.yaml => Health-Check.request.yaml} | 0 ...equest.yaml => List-Accounts.request.yaml} | 4 +-- ...st.yaml => List-Transactions.request.yaml} | 3 +-- .../Create Transaction.request.yaml | 17 ------------ ...quest.yaml => Update-Account.request.yaml} | 7 +++-- .../{General => }/Welcome.request.yaml | 0 17 files changed, 116 insertions(+), 62 deletions(-) delete mode 100644 postman/collections/Intergalactic-Bank-API/Accounts/Create Account.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Auth/Generate API Key.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Create-Account-2.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Create-Account.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Create-Transaction.request.yaml create mode 100644 postman/collections/Intergalactic-Bank-API/Delete-Account-2.request.yaml rename postman/collections/Intergalactic-Bank-API/{Accounts/Delete Account.request.yaml => Delete-Account.request.yaml} (88%) create mode 100644 postman/collections/Intergalactic-Bank-API/Generate-API-Key.request.yaml rename postman/collections/Intergalactic-Bank-API/{Accounts/Get Account.request.yaml => Get-Account.request.yaml} (89%) rename postman/collections/Intergalactic-Bank-API/{Transactions/Get Transaction.request.yaml => Get-Transaction.request.yaml} (89%) rename postman/collections/Intergalactic-Bank-API/{General/Health Check.request.yaml => Health-Check.request.yaml} (100%) rename postman/collections/Intergalactic-Bank-API/{Accounts/List Accounts.request.yaml => List-Accounts.request.yaml} (63%) rename postman/collections/Intergalactic-Bank-API/{Transactions/List Transactions.request.yaml => List-Transactions.request.yaml} (81%) delete mode 100644 postman/collections/Intergalactic-Bank-API/Transactions/Create Transaction.request.yaml rename postman/collections/Intergalactic-Bank-API/{Accounts/Update Account.request.yaml => Update-Account.request.yaml} (65%) rename postman/collections/Intergalactic-Bank-API/{General => }/Welcome.request.yaml (100%) diff --git a/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml b/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml index 97db2e6..f1f01ce 100644 --- a/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml +++ b/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml @@ -1,15 +1,23 @@ $kind: collection name: Intergalactic Bank API -description: A banking API for the Intergalactic Bank. Supports account - management and transactions. Auth via `x-api-key` header. +description: A banking API for the Intergalactic Bank. Supports account management and transactions. Auth via `x-api-key` header. auth: type: apikey credentials: - key: x-api-key - value: "{{apiKey}}" - in: header + - key: key + value: x-api-key + - key: value + value: '{{apiKey}}' + - key: in + value: header variables: - baseUrl: https://template.postman-echo.com - apiKey: "1234" - accountId: "1" - transactionId: "1" + - key: baseUrl + value: 'http://localhost:3000' + - key: apiKey + value: '' + - key: accountId + value: '' + - key: accountId2 + value: '' + - key: transactionId + value: '' \ No newline at end of file diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/Create Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Accounts/Create Account.request.yaml deleted file mode 100644 index 9aff407..0000000 --- a/postman/collections/Intergalactic-Bank-API/Accounts/Create Account.request.yaml +++ /dev/null @@ -1,17 +0,0 @@ -$kind: http-request -name: Create Account -method: POST -url: '{{baseUrl}}/api/v1/accounts' -order: 2000 -headers: - - key: Content-Type - value: application/json -body: - type: json - content: |- - { - "owner": "Nova Newman", - "currency": "COSMIC_COINS", - "balance": 0, - "accountType": "STANDARD" - } diff --git a/postman/collections/Intergalactic-Bank-API/Auth/Generate API Key.request.yaml b/postman/collections/Intergalactic-Bank-API/Auth/Generate API Key.request.yaml deleted file mode 100644 index 3301601..0000000 --- a/postman/collections/Intergalactic-Bank-API/Auth/Generate API Key.request.yaml +++ /dev/null @@ -1,7 +0,0 @@ -$kind: http-request -name: Generate API Key -method: GET -url: '{{baseUrl}}/api/v1/auth' -order: 1000 -auth: - type: noauth diff --git a/postman/collections/Intergalactic-Bank-API/Create-Account-2.request.yaml b/postman/collections/Intergalactic-Bank-API/Create-Account-2.request.yaml new file mode 100644 index 0000000..d90023c --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Create-Account-2.request.yaml @@ -0,0 +1,24 @@ +$kind: http-request +name: Create Account 2 +method: POST +url: '{{baseUrl}}/api/v1/accounts' +order: 6000 +headers: + - key: Content-Type + value: application/json +body: + type: json + content: |- + { + "owner": "Gary Galaxy", + "currency": "COSMIC_COINS", + "balance": 500, + "accountType": "PREMIUM" + } +scripts: + - type: http:afterResponse + language: text/javascript + code: |- + const jsonData = pm.response.json(); + pm.collectionVariables.set("accountId2", jsonData.account.accountId); + console.log("Account ID 2 saved:", jsonData.account.accountId); diff --git a/postman/collections/Intergalactic-Bank-API/Create-Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Create-Account.request.yaml new file mode 100644 index 0000000..5499728 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Create-Account.request.yaml @@ -0,0 +1,24 @@ +$kind: http-request +name: Create Account +method: POST +url: '{{baseUrl}}/api/v1/accounts' +order: 5000 +headers: + - key: Content-Type + value: application/json +body: + type: json + content: |- + { + "owner": "Nova Newman", + "currency": "COSMIC_COINS", + "balance": 1000, + "accountType": "STANDARD" + } +scripts: + - type: http:afterResponse + language: text/javascript + code: |- + const jsonData = pm.response.json(); + pm.collectionVariables.set("accountId", jsonData.account.accountId); + console.log("Account ID saved:", jsonData.account.accountId); diff --git a/postman/collections/Intergalactic-Bank-API/Create-Transaction.request.yaml b/postman/collections/Intergalactic-Bank-API/Create-Transaction.request.yaml new file mode 100644 index 0000000..ec4237a --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Create-Transaction.request.yaml @@ -0,0 +1,24 @@ +$kind: http-request +name: Create Transaction +method: POST +url: '{{baseUrl}}/api/v1/transactions' +order: 10000 +headers: + - key: Content-Type + value: application/json +body: + type: json + content: |- + { + "fromAccountId": "{{accountId}}", + "toAccountId": "{{accountId2}}", + "amount": 100, + "currency": "COSMIC_COINS" + } +scripts: + - type: http:afterResponse + language: text/javascript + code: |- + const jsonData = pm.response.json(); + pm.collectionVariables.set("transactionId", jsonData.transaction.transactionId); + console.log("Transaction ID saved:", jsonData.transaction.transactionId); diff --git a/postman/collections/Intergalactic-Bank-API/Delete-Account-2.request.yaml b/postman/collections/Intergalactic-Bank-API/Delete-Account-2.request.yaml new file mode 100644 index 0000000..d711907 --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Delete-Account-2.request.yaml @@ -0,0 +1,5 @@ +$kind: http-request +name: Delete Account 2 +method: DELETE +url: '{{baseUrl}}/api/v1/accounts/{{accountId2}}' +order: 13000 diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/Delete Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Delete-Account.request.yaml similarity index 88% rename from postman/collections/Intergalactic-Bank-API/Accounts/Delete Account.request.yaml rename to postman/collections/Intergalactic-Bank-API/Delete-Account.request.yaml index 8b8a366..c65d12a 100644 --- a/postman/collections/Intergalactic-Bank-API/Accounts/Delete Account.request.yaml +++ b/postman/collections/Intergalactic-Bank-API/Delete-Account.request.yaml @@ -2,4 +2,4 @@ $kind: http-request name: Delete Account method: DELETE url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' -order: 5000 +order: 12000 diff --git a/postman/collections/Intergalactic-Bank-API/Generate-API-Key.request.yaml b/postman/collections/Intergalactic-Bank-API/Generate-API-Key.request.yaml new file mode 100644 index 0000000..80e5c8a --- /dev/null +++ b/postman/collections/Intergalactic-Bank-API/Generate-API-Key.request.yaml @@ -0,0 +1,14 @@ +$kind: http-request +name: Generate API Key +method: GET +url: '{{baseUrl}}/api/v1/auth' +order: 3000 +auth: + type: noauth +scripts: + - type: http:afterResponse + language: text/javascript + code: |- + const jsonData = pm.response.json(); + pm.collectionVariables.set("apiKey", jsonData.apiKey); + console.log("API Key saved:", jsonData.apiKey); diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/Get Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Get-Account.request.yaml similarity index 89% rename from postman/collections/Intergalactic-Bank-API/Accounts/Get Account.request.yaml rename to postman/collections/Intergalactic-Bank-API/Get-Account.request.yaml index a495d1b..c691f23 100644 --- a/postman/collections/Intergalactic-Bank-API/Accounts/Get Account.request.yaml +++ b/postman/collections/Intergalactic-Bank-API/Get-Account.request.yaml @@ -2,4 +2,4 @@ $kind: http-request name: Get Account method: GET url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' -order: 3000 +order: 7000 diff --git a/postman/collections/Intergalactic-Bank-API/Transactions/Get Transaction.request.yaml b/postman/collections/Intergalactic-Bank-API/Get-Transaction.request.yaml similarity index 89% rename from postman/collections/Intergalactic-Bank-API/Transactions/Get Transaction.request.yaml rename to postman/collections/Intergalactic-Bank-API/Get-Transaction.request.yaml index 977f817..79bd340 100644 --- a/postman/collections/Intergalactic-Bank-API/Transactions/Get Transaction.request.yaml +++ b/postman/collections/Intergalactic-Bank-API/Get-Transaction.request.yaml @@ -2,4 +2,4 @@ $kind: http-request name: Get Transaction method: GET url: '{{baseUrl}}/api/v1/transactions/{{transactionId}}' -order: 3000 +order: 11000 diff --git a/postman/collections/Intergalactic-Bank-API/General/Health Check.request.yaml b/postman/collections/Intergalactic-Bank-API/Health-Check.request.yaml similarity index 100% rename from postman/collections/Intergalactic-Bank-API/General/Health Check.request.yaml rename to postman/collections/Intergalactic-Bank-API/Health-Check.request.yaml diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/List Accounts.request.yaml b/postman/collections/Intergalactic-Bank-API/List-Accounts.request.yaml similarity index 63% rename from postman/collections/Intergalactic-Bank-API/Accounts/List Accounts.request.yaml rename to postman/collections/Intergalactic-Bank-API/List-Accounts.request.yaml index 00b1b36..448d5ed 100644 --- a/postman/collections/Intergalactic-Bank-API/Accounts/List Accounts.request.yaml +++ b/postman/collections/Intergalactic-Bank-API/List-Accounts.request.yaml @@ -2,13 +2,11 @@ $kind: http-request name: List Accounts method: GET url: '{{baseUrl}}/api/v1/accounts' -order: 1000 +order: 4000 queryParams: - key: owner value: '' - description: Partial match filter on owner name disabled: true - key: createdAt value: '' - description: 'Exact date match (YYYY-MM-DD)' disabled: true diff --git a/postman/collections/Intergalactic-Bank-API/Transactions/List Transactions.request.yaml b/postman/collections/Intergalactic-Bank-API/List-Transactions.request.yaml similarity index 81% rename from postman/collections/Intergalactic-Bank-API/Transactions/List Transactions.request.yaml rename to postman/collections/Intergalactic-Bank-API/List-Transactions.request.yaml index e850015..3803998 100644 --- a/postman/collections/Intergalactic-Bank-API/Transactions/List Transactions.request.yaml +++ b/postman/collections/Intergalactic-Bank-API/List-Transactions.request.yaml @@ -2,7 +2,7 @@ $kind: http-request name: List Transactions method: GET url: '{{baseUrl}}/api/v1/transactions' -order: 1000 +order: 9000 queryParams: - key: fromAccountId value: '' @@ -12,5 +12,4 @@ queryParams: disabled: true - key: createdAt value: '' - description: 'Exact date match (YYYY-MM-DD)' disabled: true diff --git a/postman/collections/Intergalactic-Bank-API/Transactions/Create Transaction.request.yaml b/postman/collections/Intergalactic-Bank-API/Transactions/Create Transaction.request.yaml deleted file mode 100644 index 6ee199f..0000000 --- a/postman/collections/Intergalactic-Bank-API/Transactions/Create Transaction.request.yaml +++ /dev/null @@ -1,17 +0,0 @@ -$kind: http-request -name: Create Transaction -method: POST -url: '{{baseUrl}}/api/v1/transactions' -order: 2000 -headers: - - key: Content-Type - value: application/json -body: - type: json - content: |- - { - "fromAccountId": "1", - "toAccountId": "2", - "amount": 100, - "currency": "COSMIC_COINS" - } diff --git a/postman/collections/Intergalactic-Bank-API/Accounts/Update Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Update-Account.request.yaml similarity index 65% rename from postman/collections/Intergalactic-Bank-API/Accounts/Update Account.request.yaml rename to postman/collections/Intergalactic-Bank-API/Update-Account.request.yaml index de909e4..d124a31 100644 --- a/postman/collections/Intergalactic-Bank-API/Accounts/Update Account.request.yaml +++ b/postman/collections/Intergalactic-Bank-API/Update-Account.request.yaml @@ -2,7 +2,7 @@ $kind: http-request name: Update Account method: PUT url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' -order: 4000 +order: 8000 headers: - key: Content-Type value: application/json @@ -10,7 +10,6 @@ body: type: json content: |- { - "owner": "Updated Name", - "accountType": "PREMIUM", - "currency": "COSMIC_COINS" + "owner": "Nova Newman Updated", + "accountType": "PREMIUM" } diff --git a/postman/collections/Intergalactic-Bank-API/General/Welcome.request.yaml b/postman/collections/Intergalactic-Bank-API/Welcome.request.yaml similarity index 100% rename from postman/collections/Intergalactic-Bank-API/General/Welcome.request.yaml rename to postman/collections/Intergalactic-Bank-API/Welcome.request.yaml From eac2179440327ff64ccf9c51a1eabddd264f29ec Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 16:22:05 +0100 Subject: [PATCH 6/9] fix: made the API Simpler and made it a basic books API with no auth --- README.md | 126 +++---- .../Book-API/.resources/definition.yaml | 5 + .../Book-API/Books/create-book.request.yaml | 16 + .../Book-API/Books/delete-book.request.yaml | 8 + .../Books/get-book-by-id.request.yaml | 8 + .../Books/list-all-books.request.yaml | 5 + .../Book-API/Books/update-book.request.yaml | 19 ++ .../General/health-check.request.yaml} | 2 - .../General/root.request.yaml} | 4 +- .../.resources/definition.yaml | 23 -- .../Create-Account-2.request.yaml | 24 -- .../Create-Account.request.yaml | 24 -- .../Create-Transaction.request.yaml | 24 -- .../Delete-Account-2.request.yaml | 5 - .../Delete-Account.request.yaml | 5 - .../Generate-API-Key.request.yaml | 14 - .../Get-Account.request.yaml | 5 - .../Get-Transaction.request.yaml | 5 - .../List-Accounts.request.yaml | 12 - .../List-Transactions.request.yaml | 15 - .../Update-Account.request.yaml | 15 - src/database/db.js | 248 ++------------ src/middleware/auth.js | 57 ---- src/middleware/rateLimit.js | 86 ----- src/models/Account.js | 80 ----- src/models/Book.js | 36 ++ src/models/Transaction.js | 66 ---- src/routes/accounts.js | 263 -------------- src/routes/admin.js | 32 -- src/routes/books.js | 60 ++++ src/routes/transactions.js | 171 ---------- src/server.js | 101 +----- tests/integration/accounts.test.js | 230 ------------- tests/integration/admin.test.js | 43 --- tests/integration/books.test.js | 58 ++++ tests/integration/transactions.test.js | 321 ------------------ tests/integration/workflows.test.js | 120 ------- tests/unit/middleware/auth.test.js | 123 ------- tests/unit/models/Account.test.js | 228 ------------- tests/unit/models/Book.test.js | 25 ++ tests/unit/models/Transaction.test.js | 231 ------------- 41 files changed, 333 insertions(+), 2610 deletions(-) create mode 100644 postman/collections/Book-API/.resources/definition.yaml create mode 100644 postman/collections/Book-API/Books/create-book.request.yaml create mode 100644 postman/collections/Book-API/Books/delete-book.request.yaml create mode 100644 postman/collections/Book-API/Books/get-book-by-id.request.yaml create mode 100644 postman/collections/Book-API/Books/list-all-books.request.yaml create mode 100644 postman/collections/Book-API/Books/update-book.request.yaml rename postman/collections/{Intergalactic-Bank-API/Health-Check.request.yaml => Book-API/General/health-check.request.yaml} (80%) rename postman/collections/{Intergalactic-Bank-API/Welcome.request.yaml => Book-API/General/root.request.yaml} (64%) delete mode 100644 postman/collections/Intergalactic-Bank-API/.resources/definition.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Create-Account-2.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Create-Account.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Create-Transaction.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Delete-Account-2.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Delete-Account.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Generate-API-Key.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Get-Account.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Get-Transaction.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/List-Accounts.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/List-Transactions.request.yaml delete mode 100644 postman/collections/Intergalactic-Bank-API/Update-Account.request.yaml delete mode 100644 src/middleware/auth.js delete mode 100644 src/middleware/rateLimit.js delete mode 100644 src/models/Account.js create mode 100644 src/models/Book.js delete mode 100644 src/models/Transaction.js delete mode 100644 src/routes/accounts.js delete mode 100644 src/routes/admin.js create mode 100644 src/routes/books.js delete mode 100644 src/routes/transactions.js delete mode 100644 tests/integration/accounts.test.js delete mode 100644 tests/integration/admin.test.js create mode 100644 tests/integration/books.test.js delete mode 100644 tests/integration/transactions.test.js delete mode 100644 tests/integration/workflows.test.js delete mode 100644 tests/unit/middleware/auth.test.js delete mode 100644 tests/unit/models/Account.test.js create mode 100644 tests/unit/models/Book.test.js delete mode 100644 tests/unit/models/Transaction.test.js diff --git a/README.md b/README.md index 0d1b124..e62bf78 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,82 @@ -# ๐ŸŒŒ Intergalactic Bank API +# Book API -REST API for bank accounts and transactions with multi-currency support (COSMIC_COINS, GALAXY_GOLD, MOON_BUCKS). +Simple REST API for books โ€“ CRUD only, no auth. Good for demos and learning. -## Quick Start +## Quick start ```bash npm install && npm run dev -curl http://localhost:3000/health ``` -Default: **http://localhost:3000**. Generate API key: `GET /api/v1/auth`. Default admin key: `1234`. +Base URL: **http://localhost:3000** ## Endpoints -| Endpoint | Method | Auth | Description | -|----------|--------|------|-------------| -| `/health` | GET | No | Health check | -| `/api/v1/auth` | GET | No | Generate API key | -| `/api/v1/accounts` | GET, POST | Yes | List / create accounts | -| `/api/v1/accounts/:id` | GET, PUT, DELETE | Yes | Get / update / delete (soft) | -| `/api/v1/transactions` | GET, POST | Yes | List / transfer or deposit | -| `/api/v1/transactions/:id` | GET | Yes | Get transaction | +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/health` | Health check | +| GET | `/api/v1/books` | List all books | +| GET | `/api/v1/books/:id` | Get one book | +| POST | `/api/v1/books` | Create a book | +| PUT | `/api/v1/books/:id` | Update a book | +| DELETE | `/api/v1/books/:id` | Delete a book | -**Auth:** send `x-api-key: your-key` on all protected routes. **Rate limit:** 300 req/min per key. +No authentication required. -## Postman +## Examples -1. Import `OpenAPI/Bank API Reference Documentation.postman_collection.json` -2. Set `baseUrl` โ†’ `http://localhost:3000`, `apiKey` โ†’ `1234` - -## Commands - -| Command | Description | -|---------|-------------| -| `npm run dev` | Dev server (auto-reload) | -| `npm start` | Production | -| `npm test` | Run tests | -| `npm test -- --coverage` | Tests + coverage | -| `npm run lint` | Lint | - -## Config (optional `.env`) - -``` -PORT=3000 -ADMIN_API_KEY=1234 -RATE_LIMIT_REQUESTS=300 -RATE_LIMIT_WINDOW_MS=60000 -``` - -## Project Layout - -``` -src/ -โ”œโ”€โ”€ server.js # Entry -โ”œโ”€โ”€ database/db.js # In-memory store -โ”œโ”€โ”€ models/ # Account, Transaction -โ”œโ”€โ”€ routes/ # admin, accounts, transactions -โ””โ”€โ”€ middleware/ # auth, errorHandler, rateLimit +**List books** +```bash +curl http://localhost:3000/api/v1/books ``` -## Important Behavior - -- **Ownership** โ€“ Accounts are scoped to the API key that created them. -- **Soft delete** โ€“ Deleted accounts are flagged; history kept. -- **Editable** โ€“ Only `owner` and `accountType`; balance/currency change via transactions only. - -## Example Requests - -**Create account** `POST /api/v1/accounts`: -```json -{ "owner": "John Doe", "currency": "COSMIC_COINS", "balance": 1000, "accountType": "STANDARD" } +**Create book** +```bash +curl -X POST http://localhost:3000/api/v1/books \ + -H "Content-Type: application/json" \ + -d '{"title":"Dune","author":"Frank Herbert","year":1965}' ``` -**Transfer** `POST /api/v1/transactions`: -```json -{ "fromAccountId": "123", "toAccountId": "456", "amount": 500, "currency": "COSMIC_COINS" } +**Update book** (PUT with full or partial fields) +```bash +curl -X PUT http://localhost:3000/api/v1/books/1 \ + -H "Content-Type: application/json" \ + -d '{"title":"Dune","author":"Frank Herbert","year":1965}' ``` -**Deposit** โ€“ Use `"fromAccountId": "0"`. - -## Error Format - -```json -{ "error": { "name": "errorType", "message": "Description" } } +**Delete book** +```bash +curl -X DELETE http://localhost:3000/api/v1/books/1 ``` -Common codes: **400** validation, **401** auth, **403** forbidden, **404** not found, **429** rate limit, **500** server error. +## Book shape -## Sample Data (on startup) +- **title** (string, required) +- **author** (string, required) +- **year** (number, optional) -- Nova Newman (10k COSMIC_COINS), Gary Galaxy (237 COSMIC_COINS), Luna Starlight (5k GALAXY_GOLD) โ€“ all under admin key `1234`. +Responses use `{ book: {...} }` or `{ books: [...] }`. Errors use `{ error: { name, message } }`. -## Account Types & Currencies +## Commands -**Types:** STANDARD, PREMIUM, BUSINESS. **Currencies:** COSMIC_COINS, GALAXY_GOLD, MOON_BUCKS. +- `npm run dev` โ€“ dev server with reload +- `npm start` โ€“ production +- `npm test` โ€“ run tests +- `npm run lint` โ€“ lint -## Replacing Storage +## Project layout -Swap in a real DB by updating `src/database/db.js` with your driver and CRUD; the rest of the app stays the same. +``` +src/ +โ”œโ”€โ”€ server.js +โ”œโ”€โ”€ database/db.js # In-memory store +โ”œโ”€โ”€ models/Book.js +โ”œโ”€โ”€ routes/books.js +โ””โ”€โ”€ middleware/errorHandler.js +``` ---- +## Config -**More detail** โ†’ `CLAUDE.md` ยท **Tests** โ†’ `npm test` +Optional `.env`: `PORT=3000` License: ISC diff --git a/postman/collections/Book-API/.resources/definition.yaml b/postman/collections/Book-API/.resources/definition.yaml new file mode 100644 index 0000000..2094136 --- /dev/null +++ b/postman/collections/Book-API/.resources/definition.yaml @@ -0,0 +1,5 @@ +$kind: collection +name: Book API +variables: + - key: baseUrl + value: 'http://localhost:3000' diff --git a/postman/collections/Book-API/Books/create-book.request.yaml b/postman/collections/Book-API/Books/create-book.request.yaml new file mode 100644 index 0000000..091b0ca --- /dev/null +++ b/postman/collections/Book-API/Books/create-book.request.yaml @@ -0,0 +1,16 @@ +$kind: http-request +name: Create Book +method: POST +url: '{{baseUrl}}/api/v1/books' +order: 3000 +headers: + - key: Content-Type + value: application/json +body: + type: json + content: |- + { + "title": "The Great Gatsby", + "author": "F. Scott Fitzgerald", + "year": 1925 + } diff --git a/postman/collections/Book-API/Books/delete-book.request.yaml b/postman/collections/Book-API/Books/delete-book.request.yaml new file mode 100644 index 0000000..70aa60b --- /dev/null +++ b/postman/collections/Book-API/Books/delete-book.request.yaml @@ -0,0 +1,8 @@ +$kind: http-request +name: Delete Book +method: DELETE +url: '{{baseUrl}}/api/v1/books/:id' +order: 5000 +pathVariables: + - key: id + value: '1' diff --git a/postman/collections/Book-API/Books/get-book-by-id.request.yaml b/postman/collections/Book-API/Books/get-book-by-id.request.yaml new file mode 100644 index 0000000..6c69465 --- /dev/null +++ b/postman/collections/Book-API/Books/get-book-by-id.request.yaml @@ -0,0 +1,8 @@ +$kind: http-request +name: 'Get Book by ID' +method: GET +url: '{{baseUrl}}/api/v1/books/:id' +order: 2000 +pathVariables: + - key: id + value: '1' diff --git a/postman/collections/Book-API/Books/list-all-books.request.yaml b/postman/collections/Book-API/Books/list-all-books.request.yaml new file mode 100644 index 0000000..1cc813e --- /dev/null +++ b/postman/collections/Book-API/Books/list-all-books.request.yaml @@ -0,0 +1,5 @@ +$kind: http-request +name: List All Books +method: GET +url: '{{baseUrl}}/api/v1/books' +order: 1000 diff --git a/postman/collections/Book-API/Books/update-book.request.yaml b/postman/collections/Book-API/Books/update-book.request.yaml new file mode 100644 index 0000000..f7a4406 --- /dev/null +++ b/postman/collections/Book-API/Books/update-book.request.yaml @@ -0,0 +1,19 @@ +$kind: http-request +name: Update Book +method: PUT +url: '{{baseUrl}}/api/v1/books/:id' +order: 4000 +pathVariables: + - key: id + value: '1' +headers: + - key: Content-Type + value: application/json +body: + type: json + content: |- + { + "title": "The Great Gatsby (Updated)", + "author": "F. Scott Fitzgerald", + "year": 1925 + } diff --git a/postman/collections/Intergalactic-Bank-API/Health-Check.request.yaml b/postman/collections/Book-API/General/health-check.request.yaml similarity index 80% rename from postman/collections/Intergalactic-Bank-API/Health-Check.request.yaml rename to postman/collections/Book-API/General/health-check.request.yaml index 45123e7..908186a 100644 --- a/postman/collections/Intergalactic-Bank-API/Health-Check.request.yaml +++ b/postman/collections/Book-API/General/health-check.request.yaml @@ -3,5 +3,3 @@ name: Health Check method: GET url: '{{baseUrl}}/health' order: 2000 -auth: - type: noauth diff --git a/postman/collections/Intergalactic-Bank-API/Welcome.request.yaml b/postman/collections/Book-API/General/root.request.yaml similarity index 64% rename from postman/collections/Intergalactic-Bank-API/Welcome.request.yaml rename to postman/collections/Book-API/General/root.request.yaml index c93a25f..453e9b7 100644 --- a/postman/collections/Intergalactic-Bank-API/Welcome.request.yaml +++ b/postman/collections/Book-API/General/root.request.yaml @@ -1,7 +1,5 @@ $kind: http-request -name: Welcome +name: Root (Welcome) method: GET url: '{{baseUrl}}/' order: 1000 -auth: - type: noauth diff --git a/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml b/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml deleted file mode 100644 index f1f01ce..0000000 --- a/postman/collections/Intergalactic-Bank-API/.resources/definition.yaml +++ /dev/null @@ -1,23 +0,0 @@ -$kind: collection -name: Intergalactic Bank API -description: A banking API for the Intergalactic Bank. Supports account management and transactions. Auth via `x-api-key` header. -auth: - type: apikey - credentials: - - key: key - value: x-api-key - - key: value - value: '{{apiKey}}' - - key: in - value: header -variables: - - key: baseUrl - value: 'http://localhost:3000' - - key: apiKey - value: '' - - key: accountId - value: '' - - key: accountId2 - value: '' - - key: transactionId - value: '' \ No newline at end of file diff --git a/postman/collections/Intergalactic-Bank-API/Create-Account-2.request.yaml b/postman/collections/Intergalactic-Bank-API/Create-Account-2.request.yaml deleted file mode 100644 index d90023c..0000000 --- a/postman/collections/Intergalactic-Bank-API/Create-Account-2.request.yaml +++ /dev/null @@ -1,24 +0,0 @@ -$kind: http-request -name: Create Account 2 -method: POST -url: '{{baseUrl}}/api/v1/accounts' -order: 6000 -headers: - - key: Content-Type - value: application/json -body: - type: json - content: |- - { - "owner": "Gary Galaxy", - "currency": "COSMIC_COINS", - "balance": 500, - "accountType": "PREMIUM" - } -scripts: - - type: http:afterResponse - language: text/javascript - code: |- - const jsonData = pm.response.json(); - pm.collectionVariables.set("accountId2", jsonData.account.accountId); - console.log("Account ID 2 saved:", jsonData.account.accountId); diff --git a/postman/collections/Intergalactic-Bank-API/Create-Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Create-Account.request.yaml deleted file mode 100644 index 5499728..0000000 --- a/postman/collections/Intergalactic-Bank-API/Create-Account.request.yaml +++ /dev/null @@ -1,24 +0,0 @@ -$kind: http-request -name: Create Account -method: POST -url: '{{baseUrl}}/api/v1/accounts' -order: 5000 -headers: - - key: Content-Type - value: application/json -body: - type: json - content: |- - { - "owner": "Nova Newman", - "currency": "COSMIC_COINS", - "balance": 1000, - "accountType": "STANDARD" - } -scripts: - - type: http:afterResponse - language: text/javascript - code: |- - const jsonData = pm.response.json(); - pm.collectionVariables.set("accountId", jsonData.account.accountId); - console.log("Account ID saved:", jsonData.account.accountId); diff --git a/postman/collections/Intergalactic-Bank-API/Create-Transaction.request.yaml b/postman/collections/Intergalactic-Bank-API/Create-Transaction.request.yaml deleted file mode 100644 index ec4237a..0000000 --- a/postman/collections/Intergalactic-Bank-API/Create-Transaction.request.yaml +++ /dev/null @@ -1,24 +0,0 @@ -$kind: http-request -name: Create Transaction -method: POST -url: '{{baseUrl}}/api/v1/transactions' -order: 10000 -headers: - - key: Content-Type - value: application/json -body: - type: json - content: |- - { - "fromAccountId": "{{accountId}}", - "toAccountId": "{{accountId2}}", - "amount": 100, - "currency": "COSMIC_COINS" - } -scripts: - - type: http:afterResponse - language: text/javascript - code: |- - const jsonData = pm.response.json(); - pm.collectionVariables.set("transactionId", jsonData.transaction.transactionId); - console.log("Transaction ID saved:", jsonData.transaction.transactionId); diff --git a/postman/collections/Intergalactic-Bank-API/Delete-Account-2.request.yaml b/postman/collections/Intergalactic-Bank-API/Delete-Account-2.request.yaml deleted file mode 100644 index d711907..0000000 --- a/postman/collections/Intergalactic-Bank-API/Delete-Account-2.request.yaml +++ /dev/null @@ -1,5 +0,0 @@ -$kind: http-request -name: Delete Account 2 -method: DELETE -url: '{{baseUrl}}/api/v1/accounts/{{accountId2}}' -order: 13000 diff --git a/postman/collections/Intergalactic-Bank-API/Delete-Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Delete-Account.request.yaml deleted file mode 100644 index c65d12a..0000000 --- a/postman/collections/Intergalactic-Bank-API/Delete-Account.request.yaml +++ /dev/null @@ -1,5 +0,0 @@ -$kind: http-request -name: Delete Account -method: DELETE -url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' -order: 12000 diff --git a/postman/collections/Intergalactic-Bank-API/Generate-API-Key.request.yaml b/postman/collections/Intergalactic-Bank-API/Generate-API-Key.request.yaml deleted file mode 100644 index 80e5c8a..0000000 --- a/postman/collections/Intergalactic-Bank-API/Generate-API-Key.request.yaml +++ /dev/null @@ -1,14 +0,0 @@ -$kind: http-request -name: Generate API Key -method: GET -url: '{{baseUrl}}/api/v1/auth' -order: 3000 -auth: - type: noauth -scripts: - - type: http:afterResponse - language: text/javascript - code: |- - const jsonData = pm.response.json(); - pm.collectionVariables.set("apiKey", jsonData.apiKey); - console.log("API Key saved:", jsonData.apiKey); diff --git a/postman/collections/Intergalactic-Bank-API/Get-Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Get-Account.request.yaml deleted file mode 100644 index c691f23..0000000 --- a/postman/collections/Intergalactic-Bank-API/Get-Account.request.yaml +++ /dev/null @@ -1,5 +0,0 @@ -$kind: http-request -name: Get Account -method: GET -url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' -order: 7000 diff --git a/postman/collections/Intergalactic-Bank-API/Get-Transaction.request.yaml b/postman/collections/Intergalactic-Bank-API/Get-Transaction.request.yaml deleted file mode 100644 index 79bd340..0000000 --- a/postman/collections/Intergalactic-Bank-API/Get-Transaction.request.yaml +++ /dev/null @@ -1,5 +0,0 @@ -$kind: http-request -name: Get Transaction -method: GET -url: '{{baseUrl}}/api/v1/transactions/{{transactionId}}' -order: 11000 diff --git a/postman/collections/Intergalactic-Bank-API/List-Accounts.request.yaml b/postman/collections/Intergalactic-Bank-API/List-Accounts.request.yaml deleted file mode 100644 index 448d5ed..0000000 --- a/postman/collections/Intergalactic-Bank-API/List-Accounts.request.yaml +++ /dev/null @@ -1,12 +0,0 @@ -$kind: http-request -name: List Accounts -method: GET -url: '{{baseUrl}}/api/v1/accounts' -order: 4000 -queryParams: - - key: owner - value: '' - disabled: true - - key: createdAt - value: '' - disabled: true diff --git a/postman/collections/Intergalactic-Bank-API/List-Transactions.request.yaml b/postman/collections/Intergalactic-Bank-API/List-Transactions.request.yaml deleted file mode 100644 index 3803998..0000000 --- a/postman/collections/Intergalactic-Bank-API/List-Transactions.request.yaml +++ /dev/null @@ -1,15 +0,0 @@ -$kind: http-request -name: List Transactions -method: GET -url: '{{baseUrl}}/api/v1/transactions' -order: 9000 -queryParams: - - key: fromAccountId - value: '' - disabled: true - - key: toAccountId - value: '' - disabled: true - - key: createdAt - value: '' - disabled: true diff --git a/postman/collections/Intergalactic-Bank-API/Update-Account.request.yaml b/postman/collections/Intergalactic-Bank-API/Update-Account.request.yaml deleted file mode 100644 index d124a31..0000000 --- a/postman/collections/Intergalactic-Bank-API/Update-Account.request.yaml +++ /dev/null @@ -1,15 +0,0 @@ -$kind: http-request -name: Update Account -method: PUT -url: '{{baseUrl}}/api/v1/accounts/{{accountId}}' -order: 8000 -headers: - - key: Content-Type - value: application/json -body: - type: json - content: |- - { - "owner": "Nova Newman Updated", - "accountType": "PREMIUM" - } diff --git a/src/database/db.js b/src/database/db.js index efe7676..2b72908 100644 --- a/src/database/db.js +++ b/src/database/db.js @@ -1,241 +1,57 @@ /** - * In-Memory Database - * Stores accounts and transactions in memory - * In production, this would be replaced with a real database (PostgreSQL, MongoDB, etc.) + * In-memory store for books (demo only) */ const { v4: uuidv4 } = require('uuid'); -const Account = require('../models/Account'); -const Transaction = require('../models/Transaction'); +const Book = require('../models/Book'); class Database { constructor() { - console.log('๐Ÿ”„ Initializing Database...'); - this.accounts = new Map(); - this.transactions = new Map(); - this.apiKeys = new Set(['1234']); // Default API key - this.initializeSampleData(); - console.log(`โœ… Database initialized - Accounts: ${this.accounts.size}, API Keys: ${this.apiKeys.size}`); + this.books = new Map(); + this._seed(); } - /** - * Initialize with sample data for testing - */ - initializeSampleData() { - // Create sample accounts (owned by default API key '1234') - const account1 = new Account('1', 'Nova Newman', 10000, 'COSMIC_COINS', '2023-04-10', 'STANDARD', '1234', false); - const account2 = new Account('2', 'Gary Galaxy', 237, 'COSMIC_COINS', '2023-04-10', 'PREMIUM', '1234', false); - const account3 = new Account('3', 'Luna Starlight', 5000, 'GALAXY_GOLD', '2024-01-10', 'BUSINESS', '1234', false); - - this.accounts.set('1', account1); - this.accounts.set('2', account2); - this.accounts.set('3', account3); - - // Create sample transactions - const transaction1 = new Transaction('1', '1', '2', 10000, 'COSMIC_COINS', '2024-01-10'); - this.transactions.set('1', transaction1); + _seed() { + const sample = [ + new Book('1', 'The Great Gatsby', 'F. Scott Fitzgerald', 1925), + new Book('2', '1984', 'George Orwell', 1949) + ]; + sample.forEach(b => this.books.set(b.id, b)); } - // ============ Account Operations ============ - - /** - * Get all accounts with optional filters - * @param {Object} filters - Optional filters (owner, createdAt, apiKey) - * @returns {Array} - */ - getAccounts(filters = {}) { - let accounts = Array.from(this.accounts.values()); - - // Exclude deleted accounts - accounts = accounts.filter(acc => !acc.deleted); - - // Filter by API key for ownership - if (filters.apiKey) { - accounts = accounts.filter(acc => acc.apiKey === filters.apiKey); - } - - if (filters.owner) { - accounts = accounts.filter(acc => acc.owner.toLowerCase().includes(filters.owner.toLowerCase())); - } - - if (filters.createdAt) { - accounts = accounts.filter(acc => acc.createdAt === filters.createdAt); - } - - return accounts; + getBooks() { + return Array.from(this.books.values()); } - /** - * Get account by ID - * @param {string} accountId - * @returns {Account|null} - */ - getAccountById(accountId) { - return this.accounts.get(accountId) || null; + getBookById(id) { + return this.books.get(id) || null; } - /** - * Create new account - * @param {Object} accountData - * @param {string} apiKey - API key of the creator - * @returns {Account} - */ - createAccount(accountData, apiKey) { - const accountId = uuidv4().split('-')[0]; // Generate short UUID - const account = new Account( - accountId, - accountData.owner, - accountData.balance || 0, - accountData.currency, - new Date().toISOString().split('T')[0], - accountData.accountType || 'STANDARD', - apiKey, - false - ); - this.accounts.set(accountId, account); - console.log(`โœ“ Account created: ${accountId} - Total accounts: ${this.accounts.size}`); - return account; + createBook(data) { + const id = uuidv4().slice(0, 8); + const book = new Book(id, data.title.trim(), data.author.trim(), data.year ?? null); + this.books.set(id, book); + return book; } - /** - * Update account - * @param {string} accountId - * @param {Object} updates - * @returns {Account|null} - */ - updateAccount(accountId, updates) { - const account = this.accounts.get(accountId); - if (!account || account.deleted) { - return null; - } - - if (updates.owner) { - account.owner = updates.owner; - } - - if (updates.currency) { - account.currency = updates.currency; - } - - if (updates.accountType) { - account.accountType = updates.accountType; - } - - return account; + updateBook(id, data) { + const book = this.books.get(id); + if (!book) return null; + if (data.title !== undefined) book.title = data.title.trim(); + if (data.author !== undefined) book.author = data.author.trim(); + if (data.year !== undefined) book.year = data.year; + return book; } - /** - * Delete account (soft delete) - * @param {string} accountId - * @returns {boolean} - */ - deleteAccount(accountId) { - const account = this.accounts.get(accountId); - if (!account || account.deleted) { - return false; - } - account.deleted = true; - console.log(`โœ“ Account soft deleted: ${accountId}`); - return true; + deleteBook(id) { + return this.books.delete(id); } - // ============ Transaction Operations ============ - - /** - * Get all transactions with optional filters - * @param {Object} filters - Optional filters (fromAccountId, toAccountId, createdAt) - * @returns {Array} - */ - getTransactions(filters = {}) { - let transactions = Array.from(this.transactions.values()); - - if (filters.fromAccountId) { - transactions = transactions.filter(tx => tx.fromAccountId === filters.fromAccountId); - } - - if (filters.toAccountId) { - transactions = transactions.filter(tx => tx.toAccountId === filters.toAccountId); - } - - if (filters.createdAt) { - transactions = transactions.filter(tx => tx.createdAt === filters.createdAt); - } - - return transactions; - } - - /** - * Get transaction by ID - * @param {string} transactionId - * @returns {Transaction|null} - */ - getTransactionById(transactionId) { - return this.transactions.get(transactionId) || null; - } - - /** - * Check if account has any transactions - * @param {string} accountId - * @returns {boolean} - */ - accountHasTransactions(accountId) { - const transactions = Array.from(this.transactions.values()); - return transactions.some(tx => - tx.fromAccountId === accountId || tx.toAccountId === accountId - ); - } - - /** - * Create new transaction - * @param {Object} transactionData - * @returns {Transaction} - */ - createTransaction(transactionData) { - const transactionId = uuidv4().split('-')[0]; // Generate short UUID - const transaction = new Transaction( - transactionId, - transactionData.fromAccountId, - transactionData.toAccountId, - transactionData.amount, - transactionData.currency, - new Date().toISOString().split('T')[0] - ); - this.transactions.set(transactionId, transaction); - return transaction; - } - - // ============ API Key Operations ============ - - /** - * Generate new API key - * @returns {string} - */ - generateApiKey() { - const apiKey = uuidv4().replace(/-/g, '').substring(0, 16); - this.apiKeys.add(apiKey); - console.log(`โœ“ API Key generated: ${apiKey} - Total keys: ${this.apiKeys.size}`); - return apiKey; - } - - /** - * Add an API key to the database - * @param {string} apiKey - */ - addApiKey(apiKey) { - this.apiKeys.add(apiKey); - console.log(`โœ“ API Key registered: ${apiKey} - Total keys: ${this.apiKeys.size}`); - } - - /** - * Validate API key - * @param {string} apiKey - * @returns {boolean} - */ - validateApiKey(apiKey) { - return this.apiKeys.has(apiKey); + /** Reset store and re-seed (for tests) */ + reset() { + this.books.clear(); + this._seed(); } } -// Export singleton instance module.exports = new Database(); - diff --git a/src/middleware/auth.js b/src/middleware/auth.js deleted file mode 100644 index cacbf98..0000000 --- a/src/middleware/auth.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Authentication Middleware - * Validates API keys and checks permissions - */ - -const db = require('../database/db'); - -/** - * Middleware to validate API key - */ -const validateApiKey = (req, res, next) => { - - const apiKey = req.headers['x-api-key'] || req.headers['api-key']; - - if (!apiKey) { - return res.status(401).json({ - error: { - name: 'authenticationError', - message: 'API key is required. Please provide an API key in the x-api-key header.' - } - }); - } - - // If API key doesn't exist, automatically register it - if (!db.validateApiKey(apiKey)) { - db.addApiKey(apiKey); - } - - // Store API key in request for potential admin check - req.apiKey = apiKey; - next(); -}; - -/** - * Middleware to check if user has admin permissions - * For this demo, we'll use the default API key '1234' as admin - */ -const requireAdmin = (req, res, next) => { - const adminKey = process.env.ADMIN_API_KEY || '1234'; - - if (req.apiKey !== adminKey) { - return res.status(403).json({ - error: { - name: 'forbiddenError', - message: 'You do not have permissions to perform this action. Admin access required.' - } - }); - } - - next(); -}; - -module.exports = { - validateApiKey, - requireAdmin -}; - diff --git a/src/middleware/rateLimit.js b/src/middleware/rateLimit.js deleted file mode 100644 index 8c8f8e8..0000000 --- a/src/middleware/rateLimit.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Rate Limiting Middleware - * Implements simple in-memory rate limiting - */ - -class RateLimiter { - constructor(maxRequests = 300, windowMs = 60000) { - this.maxRequests = maxRequests; - this.windowMs = windowMs; - this.requests = new Map(); // Map of IP/Key -> array of timestamps - } - - /** - * Middleware function to apply rate limiting - */ - middleware() { - return (req, res, next) => { - const identifier = req.apiKey || req.ip; - const now = Date.now(); - - // Get existing requests for this identifier - if (!this.requests.has(identifier)) { - this.requests.set(identifier, []); - } - - const userRequests = this.requests.get(identifier); - - // Remove old requests outside the time window - const validRequests = userRequests.filter(timestamp => now - timestamp < this.windowMs); - - // Check if rate limit exceeded - if (validRequests.length >= this.maxRequests) { - const oldestRequest = validRequests[0]; - const resetTime = Math.ceil((oldestRequest + this.windowMs) / 1000); - - res.setHeader('X-RateLimit-Limit', this.maxRequests); - res.setHeader('X-RateLimit-Remaining', 0); - res.setHeader('X-RateLimit-Reset', resetTime); - - return res.status(429).json({ - error: { - name: 'rateLimitExceeded', - message: `Too many requests. Maximum ${this.maxRequests} requests per minute allowed.` - } - }); - } - - // Add current request - validRequests.push(now); - this.requests.set(identifier, validRequests); - - // Set rate limit headers - res.setHeader('X-RateLimit-Limit', this.maxRequests); - res.setHeader('X-RateLimit-Remaining', this.maxRequests - validRequests.length); - res.setHeader('X-RateLimit-Reset', Math.ceil((now + this.windowMs) / 1000)); - - next(); - }; - } - - /** - * Clean up old entries periodically - */ - cleanup() { - const now = Date.now(); - for (const [identifier, timestamps] of this.requests.entries()) { - const validRequests = timestamps.filter(timestamp => now - timestamp < this.windowMs); - if (validRequests.length === 0) { - this.requests.delete(identifier); - } else { - this.requests.set(identifier, validRequests); - } - } - } -} - -// Create rate limiter instance -const rateLimiterRequests = parseInt(process.env.RATE_LIMIT_REQUESTS) || 300; -const rateLimiterWindow = parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60000; -const rateLimiter = new RateLimiter(rateLimiterRequests, rateLimiterWindow); - -// Cleanup every 5 minutes -setInterval(() => rateLimiter.cleanup(), 5 * 60 * 1000); - -module.exports = rateLimiter.middleware(); - diff --git a/src/models/Account.js b/src/models/Account.js deleted file mode 100644 index 764cff7..0000000 --- a/src/models/Account.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Account Model - * Represents a bank account in the system - */ - -class Account { - constructor(accountId, owner, balance = 0, currency, createdAt = new Date().toISOString().split('T')[0], accountType = 'STANDARD', apiKey = null, deleted = false) { - this.accountId = accountId; - this.owner = owner; - this.balance = balance; - this.currency = currency; - this.createdAt = createdAt; - this.accountType = accountType; - this.apiKey = apiKey; - this.deleted = deleted; - } - - /** - * Validates account data - * @param {Object} data - Account data to validate - * @returns {Object} - Validation result with isValid and error properties - */ - static validate(data) { - const validCurrencies = ['COSMIC_COINS', 'GALAXY_GOLD', 'MOON_BUCKS']; - const validAccountTypes = ['STANDARD', 'PREMIUM', 'BUSINESS']; - - if (!data.owner || typeof data.owner !== 'string') { - return { isValid: false, error: 'Owner name is required and must be a string' }; - } - - if (!data.currency || !validCurrencies.includes(data.currency)) { - return { isValid: false, error: `Currency must be one of: ${validCurrencies.join(', ')}` }; - } - - if (data.balance !== undefined && (typeof data.balance !== 'number' || data.balance < 0)) { - return { isValid: false, error: 'Balance must be a non-negative number' }; - } - - if (data.accountType && !validAccountTypes.includes(data.accountType)) { - return { isValid: false, error: `Account type must be one of: ${validAccountTypes.join(', ')}` }; - } - - return { isValid: true }; - } - - /** - * Updates account balance - * @param {number} amount - Amount to add (positive) or subtract (negative) - */ - updateBalance(amount) { - this.balance += amount; - } - - /** - * Checks if account has sufficient funds - * @param {number} amount - Amount to check - * @returns {boolean} - */ - hasSufficientFunds(amount) { - return this.balance >= amount; - } - - /** - * Converts account to JSON representation - * @returns {Object} - */ - toJSON() { - return { - accountId: this.accountId, - owner: this.owner, - accountType: this.accountType, - createdAt: this.createdAt, - balance: this.balance, - currency: this.currency - }; - } -} - -module.exports = Account; - diff --git a/src/models/Book.js b/src/models/Book.js new file mode 100644 index 0000000..8711403 --- /dev/null +++ b/src/models/Book.js @@ -0,0 +1,36 @@ +/** + * Book model โ€“ title, author, optional year + */ + +class Book { + constructor(id, title, author, year = null) { + this.id = id; + this.title = title; + this.author = author; + this.year = year; + } + + static validate(data) { + if (!data.title || typeof data.title !== 'string' || !data.title.trim()) { + return { isValid: false, error: 'Title is required' }; + } + if (!data.author || typeof data.author !== 'string' || !data.author.trim()) { + return { isValid: false, error: 'Author is required' }; + } + if (data.year != null && (typeof data.year !== 'number' || data.year < 0 || !Number.isInteger(data.year))) { + return { isValid: false, error: 'Year must be a non-negative integer' }; + } + return { isValid: true }; + } + + toJSON() { + return { + id: this.id, + title: this.title, + author: this.author, + year: this.year + }; + } +} + +module.exports = Book; diff --git a/src/models/Transaction.js b/src/models/Transaction.js deleted file mode 100644 index 1fb9086..0000000 --- a/src/models/Transaction.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Transaction Model - * Represents a transaction between accounts - */ - -class Transaction { - constructor(transactionId, fromAccountId, toAccountId, amount, currency, createdAt = new Date().toISOString().split('T')[0]) { - this.transactionId = transactionId; - this.fromAccountId = fromAccountId; - this.toAccountId = toAccountId; - this.amount = amount; - this.currency = currency; - this.createdAt = createdAt; - } - - /** - * Validates transaction data - * @param {Object} data - Transaction data to validate - * @returns {Object} - Validation result with isValid and error properties - */ - static validate(data) { - if (!data.fromAccountId && data.fromAccountId !== '0') { - return { isValid: false, error: 'fromAccountId is required' }; - } - - if (!data.toAccountId) { - return { isValid: false, error: 'toAccountId is required' }; - } - - if (!data.amount || typeof data.amount !== 'number' || data.amount <= 0) { - return { isValid: false, error: 'Amount must be a positive number' }; - } - - if (!data.currency) { - return { isValid: false, error: 'Currency is required' }; - } - - return { isValid: true }; - } - - /** - * Checks if transaction is a deposit (from external source) - * @returns {boolean} - */ - isDeposit() { - return this.fromAccountId === '0'; - } - - /** - * Converts transaction to JSON representation - * @returns {Object} - */ - toJSON() { - return { - transactionId: this.transactionId, - createdAt: this.createdAt, - amount: this.amount, - currency: this.currency, - fromAccountId: this.fromAccountId, - toAccountId: this.toAccountId - }; - } -} - -module.exports = Transaction; - diff --git a/src/routes/accounts.js b/src/routes/accounts.js deleted file mode 100644 index 10311fc..0000000 --- a/src/routes/accounts.js +++ /dev/null @@ -1,263 +0,0 @@ -/** - * Account Routes - * Handles all account-related operations - */ - -const express = require('express'); -const router = express.Router(); -const db = require('../database/db'); -const Account = require('../models/Account'); -const { validateApiKey } = require('../middleware/auth'); - -/** - * GET /api/v1/accounts - * List all accounts with optional filters - */ -router.get('/', validateApiKey, (req, res) => { - try { - const { owner, createdAt } = req.query; - - // Filter by API key for ownership and other optional filters - const filters = { apiKey: req.apiKey }; - if (owner) filters.owner = owner; - if (createdAt) filters.createdAt = createdAt; - - const accounts = db.getAccounts(filters); - - res.status(200).json({ - accounts: accounts.map(acc => acc.toJSON()) - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to retrieve accounts' - } - }); - } -}); - -/** - * POST /api/v1/accounts - * Create a new account - */ -router.post('/', validateApiKey, (req, res) => { - try { - const accountData = req.body; - - // Validate account data - const validation = Account.validate(accountData); - if (!validation.isValid) { - return res.status(400).json({ - error: { - name: 'validationError', - message: validation.error - } - }); - } - - // Create account with API key for ownership - const account = db.createAccount(accountData, req.apiKey); - - res.status(201).json({ - account: { - accountId: account.accountId - } - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to create account' - } - }); - } -}); - -/** - * GET /api/v1/accounts/:id - * Get a single account by ID - */ -router.get('/:id', validateApiKey, (req, res) => { - try { - const { id } = req.params; - - // Get account from database - const account = db.getAccountById(id); - - // Check if account exists - if (!account || account.deleted) { - return res.status(404).json({ - error: { - name: 'notFound', - message: 'Account not found' - } - }); - } - - // Check ownership - users can only access their own accounts - if (account.apiKey !== req.apiKey) { - return res.status(403).json({ - error: { - name: 'forbidden', - message: 'Access denied. You can only access your own accounts.' - } - }); - } - - res.status(200).json({ - account: account.toJSON() - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to retrieve account' - } - }); - } -}); - -/** - * PUT /api/v1/accounts/:id - * Update an existing account - */ -router.put('/:id', validateApiKey, (req, res) => { - try { - const { id } = req.params; - const updates = req.body; - - // Get account from database - const account = db.getAccountById(id); - - // Check if account exists - if (!account || account.deleted) { - return res.status(404).json({ - error: { - name: 'notFound', - message: 'Account not found' - } - }); - } - - // Check ownership - users can only update their own accounts - if (account.apiKey !== req.apiKey) { - return res.status(403).json({ - error: { - name: 'forbidden', - message: 'Access denied. You can only update your own accounts.' - } - }); - } - - // Prevent balance updates (balance only changes via transactions) - if (updates.balance !== undefined) { - return res.status(400).json({ - error: { - name: 'validationError', - message: 'Balance cannot be updated directly. Use transaction endpoints to modify balance.' - } - }); - } - - // Prevent updates to immutable fields - if (updates.accountId || updates.createdAt || updates.apiKey || updates.deleted !== undefined) { - return res.status(400).json({ - error: { - name: 'validationError', - message: 'Cannot update accountId, createdAt, apiKey, or deleted fields.' - } - }); - } - - // Validate updatable fields - const validationData = { ...account, ...updates }; - const validation = Account.validate(validationData); - if (!validation.isValid) { - return res.status(400).json({ - error: { - name: 'validationError', - message: validation.error - } - }); - } - - // Update account - const updatedAccount = db.updateAccount(id, updates); - - res.status(200).json({ - account: updatedAccount.toJSON() - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to update account' - } - }); - } -}); - -/** - * DELETE /api/v1/accounts/:id - * Delete an account (soft delete) - */ -router.delete('/:id', validateApiKey, (req, res) => { - try { - const { id } = req.params; - - // Get account from database - const account = db.getAccountById(id); - - // Check if account exists - if (!account || account.deleted) { - return res.status(404).json({ - error: { - name: 'notFound', - message: 'Account not found' - } - }); - } - - // Check ownership - users can only delete their own accounts - if (account.apiKey !== req.apiKey) { - return res.status(403).json({ - error: { - name: 'forbidden', - message: 'Access denied. You can only delete your own accounts.' - } - }); - } - - // Check if account has transactions - const hasTransactions = db.accountHasTransactions(id); - - // Always perform soft delete - const deleted = db.deleteAccount(id); - - if (!deleted) { - return res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to delete account' - } - }); - } - - res.status(200).json({ - message: 'Account deleted successfully', - accountId: id, - deletionType: hasTransactions ? 'soft (has transactions)' : 'soft' - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to delete account' - } - }); - } -}); - -module.exports = router; - diff --git a/src/routes/admin.js b/src/routes/admin.js deleted file mode 100644 index 86169cb..0000000 --- a/src/routes/admin.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Admin Routes - * Handles administrative operations like API key generation - */ - -const express = require('express'); -const router = express.Router(); -const db = require('../database/db'); - -/** - * GET /api/v1/auth - * Generate a new API key - */ -router.get('/auth', (req, res) => { - try { - const apiKey = db.generateApiKey(); - - res.status(200).json({ - apiKey: apiKey - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to generate API key' - } - }); - } -}); - -module.exports = router; - diff --git a/src/routes/books.js b/src/routes/books.js new file mode 100644 index 0000000..102744e --- /dev/null +++ b/src/routes/books.js @@ -0,0 +1,60 @@ +/** + * Book CRUD routes โ€“ no auth + */ + +const express = require('express'); +const router = express.Router(); +const db = require('../database/db'); +const Book = require('../models/Book'); + +// List books +router.get('/', (req, res) => { + const books = db.getBooks(); + res.json({ books: books.map(b => b.toJSON()) }); +}); + +// Get one book +router.get('/:id', (req, res) => { + const book = db.getBookById(req.params.id); + if (!book) { + return res.status(404).json({ error: { name: 'notFound', message: 'Book not found' } }); + } + res.json({ book: book.toJSON() }); +}); + +// Create book +router.post('/', (req, res) => { + const validation = Book.validate(req.body); + if (!validation.isValid) { + return res.status(400).json({ error: { name: 'validationError', message: validation.error } }); + } + const book = db.createBook(req.body); + res.status(201).json({ book: book.toJSON() }); +}); + +// Update book +router.put('/:id', (req, res) => { + const book = db.getBookById(req.params.id); + if (!book) { + return res.status(404).json({ error: { name: 'notFound', message: 'Book not found' } }); + } + const updates = req.body; + const merged = { title: updates.title ?? book.title, author: updates.author ?? book.author, year: updates.year !== undefined ? updates.year : book.year }; + const validation = Book.validate(merged); + if (!validation.isValid) { + return res.status(400).json({ error: { name: 'validationError', message: validation.error } }); + } + const updated = db.updateBook(req.params.id, updates); + res.json({ book: updated.toJSON() }); +}); + +// Delete book +router.delete('/:id', (req, res) => { + const ok = db.deleteBook(req.params.id); + if (!ok) { + return res.status(404).json({ error: { name: 'notFound', message: 'Book not found' } }); + } + res.status(200).json({ message: 'Book deleted', id: req.params.id }); +}); + +module.exports = router; diff --git a/src/routes/transactions.js b/src/routes/transactions.js deleted file mode 100644 index a6f3e54..0000000 --- a/src/routes/transactions.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Transaction Routes - * Handles all transaction-related operations - */ - -const express = require('express'); -const router = express.Router(); -const db = require('../database/db'); -const Transaction = require('../models/Transaction'); -const { validateApiKey } = require('../middleware/auth'); - -/** - * GET /api/v1/transactions - * List all transactions with optional filters - */ -router.get('/', validateApiKey, (req, res) => { - try { - const { fromAccountId, toAccountId, createdAt } = req.query; - - const filters = {}; - if (fromAccountId) filters.fromAccountId = fromAccountId; - if (toAccountId) filters.toAccountId = toAccountId; - if (createdAt) filters.createdAt = createdAt; - - const transactions = db.getTransactions(filters); - - res.status(200).json({ - transactions: transactions.map(tx => tx.toJSON()) - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to retrieve transactions' - } - }); - } -}); - -/** - * GET /api/v1/transactions/:transactionId - * Get a specific transaction by ID - */ -router.get('/:transactionId', validateApiKey, (req, res) => { - try { - const { transactionId } = req.params; - - const transaction = db.getTransactionById(transactionId); - - if (!transaction) { - return res.status(404).json({ - error: { - name: 'instanceNotFoundError', - message: 'The specified transaction does not exist.' - } - }); - } - - res.status(200).json({ - transaction: transaction.toJSON() - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to retrieve transaction' - } - }); - } -}); - -/** - * POST /api/v1/transactions - * Create a new transaction (transfer or deposit) - */ -router.post('/', validateApiKey, (req, res) => { - try { - const transactionData = req.body; - - // Validate transaction data - const validation = Transaction.validate(transactionData); - if (!validation.isValid) { - return res.status(400).json({ - error: { - name: 'validationError', - message: validation.error - } - }); - } - - // Check if destination account exists - const toAccount = db.getAccountById(transactionData.toAccountId); - if (!toAccount) { - return res.status(404).json({ - error: { - name: 'instanceNotFoundError', - message: 'Destination account does not exist.' - } - }); - } - - // Check if currencies match - if (toAccount.currency !== transactionData.currency) { - return res.status(400).json({ - error: { - name: 'validationError', - message: `Currency mismatch. Account uses ${toAccount.currency} but transaction uses ${transactionData.currency}` - } - }); - } - - // If not a deposit, validate source account and check funds - if (transactionData.fromAccountId !== '0') { - const fromAccount = db.getAccountById(transactionData.fromAccountId); - - if (!fromAccount) { - return res.status(404).json({ - error: { - name: 'instanceNotFoundError', - message: 'Source account does not exist.' - } - }); - } - - // Check if source account has sufficient funds - if (!fromAccount.hasSufficientFunds(transactionData.amount)) { - return res.status(403).json({ - error: { - name: 'txInsufficientFunds', - message: 'Not enough funds in source account to complete transaction.' - } - }); - } - - // Check if currencies match - if (fromAccount.currency !== transactionData.currency) { - return res.status(400).json({ - error: { - name: 'validationError', - message: 'Currency mismatch between accounts' - } - }); - } - - // Deduct from source account - fromAccount.updateBalance(-transactionData.amount); - } - - // Add to destination account - toAccount.updateBalance(transactionData.amount); - - // Create transaction record - const transaction = db.createTransaction(transactionData); - - res.status(201).json({ - transaction: { - transactionId: transaction.transactionId - } - }); - } catch (error) { - res.status(500).json({ - error: { - name: 'serverError', - message: 'Failed to create transaction' - } - }); - } -}); - -module.exports = router; - diff --git a/src/server.js b/src/server.js index 96e08b5..8b316d3 100644 --- a/src/server.js +++ b/src/server.js @@ -1,116 +1,33 @@ -/** - * Intergalactic Bank API Server - * Main server file that initializes and starts the Express application - */ - require('dotenv').config(); const express = require('express'); const cors = require('cors'); - -// Import middleware -const rateLimit = require('./middleware/rateLimit'); const { errorHandler, notFoundHandler } = require('./middleware/errorHandler'); +const bookRoutes = require('./routes/books'); -// Import routes -const adminRoutes = require('./routes/admin'); -const accountRoutes = require('./routes/accounts'); -const transactionRoutes = require('./routes/transactions'); - -// Initialize Express app const app = express(); const PORT = process.env.PORT || 3000; -// ============ Global Middleware ============ - -// CORS - Allow all origins (configure as needed for production) app.use(cors()); - -// Body parser - Parse JSON request bodies app.use(express.json()); -app.use(express.urlencoded({ extended: true })); - -// Rate limiting - Apply to all routes -app.use(rateLimit); - -// Request logging (simple) -app.use((req, res, next) => { - console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); - next(); -}); - -// ============ API Routes ============ -// Health check endpoint app.get('/health', (req, res) => { - res.status(200).json({ - status: 'healthy', - timestamp: new Date().toISOString(), - uptime: process.uptime() - }); + res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); -// Welcome endpoint app.get('/', (req, res) => { - res.status(200).json({ - message: 'Welcome to the Intergalactic Bank API! ๐ŸŒŒ', - version: '1.0.0', - documentation: 'See README.md for API documentation', - endpoints: { - health: 'GET /health', - auth: 'GET /api/v1/auth', - accounts: 'GET /api/v1/accounts', - transactions: 'GET /api/v1/transactions' - } - }); + res.json({ message: 'Book API', docs: 'GET /api/v1/books' }); }); -// Mount API routes -app.use('/api/v1', adminRoutes); -app.use('/api/v1/accounts', accountRoutes); -app.use('/api/v1/transactions', transactionRoutes); - -// ============ Error Handling ============ +app.use('/api/v1/books', bookRoutes); -// 404 handler for undefined routes app.use(notFoundHandler); - -// Global error handler app.use(errorHandler); -// ============ Start Server ============ - -const server = app.listen(PORT, () => { - console.log('โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); - console.log('โ•‘ ๐ŸŒŒ Intergalactic Bank API Server ๐ŸŒŒ โ•‘'); - console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); - console.log(''); - console.log(`๐Ÿš€ Server running on port ${PORT}`); - console.log(`๐Ÿ“ก Environment: ${process.env.NODE_ENV || 'development'}`); - console.log(`๐Ÿ”— URL: http://localhost:${PORT}`); - console.log(`๐Ÿ’š Health Check: http://localhost:${PORT}/health`); - console.log(''); - console.log('Available endpoints:'); - console.log(' - GET /api/v1/auth'); - console.log(' - GET /api/v1/accounts'); - console.log(' - POST /api/v1/accounts'); - console.log(' - GET /api/v1/accounts/:accountId'); - console.log(' - PATCH /api/v1/accounts/:accountId'); - console.log(' - DELETE /api/v1/accounts/:accountId'); - console.log(' - GET /api/v1/transactions'); - console.log(' - POST /api/v1/transactions'); - console.log(' - GET /api/v1/transactions/:transactionId'); - console.log(''); - console.log('Press CTRL+C to stop the server'); - console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); -}); - -// Graceful shutdown -process.on('SIGTERM', () => { - console.log('SIGTERM signal received: closing HTTP server'); - server.close(() => { - console.log('HTTP server closed'); +if (require.main === module) { + const server = app.listen(PORT, () => { + console.log(`Book API running at http://localhost:${PORT}`); }); -}); + process.on('SIGTERM', () => server.close(() => console.log('Server closed'))); +} module.exports = app; - diff --git a/tests/integration/accounts.test.js b/tests/integration/accounts.test.js deleted file mode 100644 index 1f89d80..0000000 --- a/tests/integration/accounts.test.js +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Account Routes Integration Tests - */ - -const request = require('supertest'); -const express = require('express'); -const accountRoutes = require('../../src/routes/accounts'); - -// Mock auth middleware -jest.mock('../../src/middleware/auth', () => ({ - validateApiKey: (req, res, next) => { - req.apiKey = req.headers['x-api-key'] || 'test-key'; - next(); - }, - requireAdmin: (req, res, next) => { - if (req.apiKey === 'admin-key') { - next(); - } else { - res.status(403).json({ - error: { - name: 'forbiddenError', - message: 'You do not have permissions to perform this action. Admin access required.' - } - }); - } - } -})); - -const db = require('../../src/database/db'); - -// Create fresh database for each test -beforeEach(() => { - // Clear and reinitialize - db.accounts.clear(); - db.transactions.clear(); - db.apiKeys.clear(); - db.apiKeys.add('1234'); // Re-add default API key - db.initializeSampleData(); -}); - -// Create test app -const app = express(); -app.use(express.json()); -app.use('/api/v1/accounts', accountRoutes); - -describe('Account Routes', () => { - describe('GET /api/v1/accounts', () => { - test('should return all accounts', async () => { - const response = await request(app) - .get('/api/v1/accounts') - .set('x-api-key', 'test-key') - .expect(200); - - expect(response.body).toHaveProperty('accounts'); - expect(Array.isArray(response.body.accounts)).toBe(true); - expect(response.body.accounts.length).toBeGreaterThan(0); - }); - - test('should filter accounts by owner', async () => { - const response = await request(app) - .get('/api/v1/accounts?owner=Nova') - .set('x-api-key', 'test-key') - .expect(200); - - expect(response.body.accounts.length).toBeGreaterThan(0); - expect(response.body.accounts[0].owner).toContain('Nova'); - }); - - test('should filter accounts by createdAt', async () => { - const response = await request(app) - .get('/api/v1/accounts?createdAt=2023-04-10') - .set('x-api-key', 'test-key') - .expect(200); - - expect(Array.isArray(response.body.accounts)).toBe(true); - response.body.accounts.forEach(account => { - expect(account.createdAt).toBe('2023-04-10'); - }); - }); - - test('should return empty array for non-matching filters', async () => { - const response = await request(app) - .get('/api/v1/accounts?owner=NonExistent') - .set('x-api-key', 'test-key') - .expect(200); - - expect(response.body.accounts).toEqual([]); - }); - }); - - describe('GET /api/v1/accounts/:accountId', () => { - }); - - describe('POST /api/v1/accounts', () => { - test('should create a new account', async () => { - const newAccount = { - owner: 'Test User', - balance: 5000, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/accounts') - .set('x-api-key', 'test-key') - .send(newAccount) - .expect(201); - - expect(response.body).toHaveProperty('account'); - expect(response.body.account).toHaveProperty('accountId'); - }); - - test('should reject account with missing owner', async () => { - const invalidAccount = { - balance: 5000, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/accounts') - .set('x-api-key', 'test-key') - .send(invalidAccount) - .expect(400); - - expect(response.body.error.name).toBe('validationError'); - }); - - test('should reject account with invalid currency', async () => { - const invalidAccount = { - owner: 'Test User', - balance: 5000, - currency: 'INVALID_CURRENCY' - }; - - const response = await request(app) - .post('/api/v1/accounts') - .set('x-api-key', 'test-key') - .send(invalidAccount) - .expect(400); - - expect(response.body.error.name).toBe('validationError'); - }); - - test('should reject account with negative balance', async () => { - const invalidAccount = { - owner: 'Test User', - balance: -100, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/accounts') - .set('x-api-key', 'test-key') - .send(invalidAccount) - .expect(400); - - expect(response.body.error.name).toBe('validationError'); - }); - }); - - describe('PATCH /api/v1/accounts/:accountId', () => { - test('should update account with admin key', async () => { - const updates = { - owner: 'Updated Name' - }; - - const response = await request(app) - .patch('/api/v1/accounts/1') - .set('x-api-key', 'admin-key') - .send(updates) - .expect(200); - - expect(response.body.account.owner).toBe('Updated Name'); - expect(response.body.account.accountId).toBe('1'); - }); - - test('should reject update without admin key', async () => { - const updates = { - owner: 'Updated Name' - }; - - await request(app) - .patch('/api/v1/accounts/1') - .set('x-api-key', 'regular-key') - .send(updates) - .expect(403); - }); - - test('should return 404 for non-existent account', async () => { - const updates = { - owner: 'Updated Name' - }; - - await request(app) - .patch('/api/v1/accounts/999') - .set('x-api-key', 'admin-key') - .send(updates) - .expect(404); - }); - }); - - describe('DELETE /api/v1/accounts/:accountId', () => { - test('should delete account with admin key', async () => { - await request(app) - .delete('/api/v1/accounts/1') - .set('x-api-key', 'admin-key') - .expect(204); - - // Verify account is deleted - await request(app) - .get('/api/v1/accounts/1') - .set('x-api-key', 'test-key') - .expect(404); - }); - - test('should reject delete without admin key', async () => { - await request(app) - .delete('/api/v1/accounts/1') - .set('x-api-key', 'regular-key') - .expect(403); - }); - - test('should return 404 for non-existent account', async () => { - await request(app) - .delete('/api/v1/accounts/999') - .set('x-api-key', 'admin-key') - .expect(404); - }); - }); -}); - diff --git a/tests/integration/admin.test.js b/tests/integration/admin.test.js deleted file mode 100644 index 72d814c..0000000 --- a/tests/integration/admin.test.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Admin Routes Integration Tests - */ - -const request = require('supertest'); -const express = require('express'); -const adminRoutes = require('../../src/routes/admin'); - -// Create test app -const app = express(); -app.use(express.json()); -app.use('/api/v1', adminRoutes); - -describe('Admin Routes', () => { - describe('GET /api/v1/auth', () => { - test('should generate a new API key', async () => { - const response = await request(app) - .get('/api/v1/auth') - .expect(200); - - expect(response.body).toHaveProperty('apiKey'); - expect(typeof response.body.apiKey).toBe('string'); - expect(response.body.apiKey.length).toBeGreaterThan(0); - }); - - test('should return valid JSON', async () => { - const response = await request(app) - .get('/api/v1/auth') - .expect('Content-Type', /json/) - .expect(200); - - expect(response.body).toBeInstanceOf(Object); - }); - - test('should generate different API keys on subsequent requests', async () => { - const response1 = await request(app).get('/api/v1/auth'); - const response2 = await request(app).get('/api/v1/auth'); - - expect(response1.body.apiKey).not.toBe(response2.body.apiKey); - }); - }); -}); - diff --git a/tests/integration/books.test.js b/tests/integration/books.test.js new file mode 100644 index 0000000..8eab02e --- /dev/null +++ b/tests/integration/books.test.js @@ -0,0 +1,58 @@ +const request = require('supertest'); +const app = require('../../src/server'); +const db = require('../../src/database/db'); + +beforeEach(() => { + db.reset(); +}); + +describe('Book API', () => { + test('GET /api/v1/books returns list', async () => { + const res = await request(app).get('/api/v1/books').expect(200); + expect(res.body.books).toBeInstanceOf(Array); + expect(res.body.books.length).toBeGreaterThanOrEqual(2); + }); + + test('GET /api/v1/books/:id returns one book', async () => { + const res = await request(app).get('/api/v1/books/1').expect(200); + expect(res.body.book).toMatchObject({ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 }); + }); + + test('GET /api/v1/books/:id returns 404 for missing id', async () => { + await request(app).get('/api/v1/books/nonexistent').expect(404); + }); + + test('POST /api/v1/books creates a book', async () => { + const res = await request(app) + .post('/api/v1/books') + .send({ title: 'Dune', author: 'Frank Herbert', year: 1965 }) + .expect(201); + expect(res.body.book).toMatchObject({ title: 'Dune', author: 'Frank Herbert', year: 1965 }); + expect(res.body.book.id).toBeDefined(); + }); + + test('POST /api/v1/books returns 400 without title', async () => { + await request(app) + .post('/api/v1/books') + .send({ author: 'Someone' }) + .expect(400); + }); + + test('PUT /api/v1/books/:id updates a book', async () => { + const res = await request(app) + .put('/api/v1/books/1') + .send({ title: 'The Great Gatsby (Updated)', year: 1925 }) + .expect(200); + expect(res.body.book.title).toBe('The Great Gatsby (Updated)'); + }); + + test('DELETE /api/v1/books/:id removes book', async () => { + await request(app).delete('/api/v1/books/1').expect(200); + await request(app).get('/api/v1/books/1').expect(404); + }); + + test('GET /health returns ok', async () => { + const res = await request(app).get('/health').expect(200); + expect(res.body.status).toBe('ok'); + }); +}); diff --git a/tests/integration/transactions.test.js b/tests/integration/transactions.test.js deleted file mode 100644 index 6901f89..0000000 --- a/tests/integration/transactions.test.js +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Transaction Routes Integration Tests - */ - -const request = require('supertest'); -const express = require('express'); -const transactionRoutes = require('../../src/routes/transactions'); - -// Mock auth middleware -jest.mock('../../src/middleware/auth', () => ({ - validateApiKey: (req, res, next) => { - req.apiKey = req.headers['x-api-key'] || 'test-key'; - next(); - } -})); - -const db = require('../../src/database/db'); - -// Create fresh database for each test -beforeEach(() => { - // Clear and reinitialize - db.accounts.clear(); - db.transactions.clear(); - db.apiKeys.clear(); - db.apiKeys.add('1234'); // Re-add default API key - db.initializeSampleData(); -}); - -// Create test app -const app = express(); -app.use(express.json()); -app.use('/api/v1/transactions', transactionRoutes); - -describe('Transaction Routes', () => { - describe('GET /api/v1/transactions', () => { - test('should return all transactions', async () => { - const response = await request(app) - .get('/api/v1/transactions') - .set('x-api-key', 'test-key') - .expect(200); - - expect(response.body).toHaveProperty('transactions'); - expect(Array.isArray(response.body.transactions)).toBe(true); - }); - - test('should filter transactions by fromAccountId', async () => { - const response = await request(app) - .get('/api/v1/transactions?fromAccountId=1') - .set('x-api-key', 'test-key') - .expect(200); - - expect(Array.isArray(response.body.transactions)).toBe(true); - response.body.transactions.forEach(tx => { - expect(tx.fromAccountId).toBe('1'); - }); - }); - - test('should filter transactions by toAccountId', async () => { - const response = await request(app) - .get('/api/v1/transactions?toAccountId=2') - .set('x-api-key', 'test-key') - .expect(200); - - expect(Array.isArray(response.body.transactions)).toBe(true); - response.body.transactions.forEach(tx => { - expect(tx.toAccountId).toBe('2'); - }); - }); - - test('should filter transactions by createdAt', async () => { - const response = await request(app) - .get('/api/v1/transactions?createdAt=2024-01-10') - .set('x-api-key', 'test-key') - .expect(200); - - expect(Array.isArray(response.body.transactions)).toBe(true); - response.body.transactions.forEach(tx => { - expect(tx.createdAt).toBe('2024-01-10'); - }); - }); - - test('should return empty array for non-matching filters', async () => { - const response = await request(app) - .get('/api/v1/transactions?fromAccountId=999') - .set('x-api-key', 'test-key') - .expect(200); - - expect(response.body.transactions).toEqual([]); - }); - }); - - describe('GET /api/v1/transactions/:transactionId', () => { - test('should return a specific transaction', async () => { - const response = await request(app) - .get('/api/v1/transactions/1') - .set('x-api-key', 'test-key') - .expect(200); - - expect(response.body).toHaveProperty('transaction'); - expect(response.body.transaction.transactionId).toBe('1'); - expect(response.body.transaction).toHaveProperty('amount'); - expect(response.body.transaction).toHaveProperty('currency'); - expect(response.body.transaction).toHaveProperty('fromAccountId'); - expect(response.body.transaction).toHaveProperty('toAccountId'); - }); - - test('should return 404 for non-existent transaction', async () => { - const response = await request(app) - .get('/api/v1/transactions/999') - .set('x-api-key', 'test-key') - .expect(404); - - expect(response.body.error.name).toBe('instanceNotFoundError'); - }); - }); - - describe('POST /api/v1/transactions', () => { - test('should create a transfer transaction', async () => { - const transaction = { - fromAccountId: '1', - toAccountId: '2', - amount: 500, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(201); - - expect(response.body).toHaveProperty('transaction'); - expect(response.body.transaction).toHaveProperty('transactionId'); - }); - - test('should update balances after transfer', async () => { - // Get initial balances - const account1Before = db.getAccountById('1'); - const account2Before = db.getAccountById('2'); - const initialBalance1 = account1Before.balance; - const initialBalance2 = account2Before.balance; - - const transaction = { - fromAccountId: '1', - toAccountId: '2', - amount: 500, - currency: 'COSMIC_COINS' - }; - - await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(201); - - // Check updated balances - const account1After = db.getAccountById('1'); - const account2After = db.getAccountById('2'); - - expect(account1After.balance).toBe(initialBalance1 - 500); - expect(account2After.balance).toBe(initialBalance2 + 500); - }); - - test('should create a deposit transaction', async () => { - const transaction = { - fromAccountId: '0', - toAccountId: '2', - amount: 1000, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(201); - - expect(response.body.transaction).toHaveProperty('transactionId'); - }); - - test('should update balance after deposit', async () => { - const accountBefore = db.getAccountById('2'); - const initialBalance = accountBefore.balance; - - const transaction = { - fromAccountId: '0', - toAccountId: '2', - amount: 1000, - currency: 'COSMIC_COINS' - }; - - await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(201); - - const accountAfter = db.getAccountById('2'); - expect(accountAfter.balance).toBe(initialBalance + 1000); - }); - - test('should reject transaction with insufficient funds', async () => { - const transaction = { - fromAccountId: '1', - toAccountId: '2', - amount: 999999999, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(403); - - expect(response.body.error.name).toBe('txInsufficientFunds'); - }); - - test('should reject transaction with non-existent source account', async () => { - const transaction = { - fromAccountId: '999', - toAccountId: '2', - amount: 500, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(404); - - expect(response.body.error.name).toBe('instanceNotFoundError'); - }); - - test('should reject transaction with non-existent destination account', async () => { - const transaction = { - fromAccountId: '1', - toAccountId: '999', - amount: 500, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(404); - - expect(response.body.error.name).toBe('instanceNotFoundError'); - }); - - test('should reject transaction with currency mismatch', async () => { - const transaction = { - fromAccountId: '1', - toAccountId: '2', - amount: 500, - currency: 'GALAXY_GOLD' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(400); - - expect(response.body.error.name).toBe('validationError'); - }); - - test('should reject transaction with missing amount', async () => { - const transaction = { - fromAccountId: '1', - toAccountId: '2', - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(400); - - expect(response.body.error.name).toBe('validationError'); - }); - - test('should reject transaction with zero amount', async () => { - const transaction = { - fromAccountId: '1', - toAccountId: '2', - amount: 0, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(400); - - expect(response.body.error.name).toBe('validationError'); - }); - - test('should reject transaction with negative amount', async () => { - const transaction = { - fromAccountId: '1', - toAccountId: '2', - amount: -500, - currency: 'COSMIC_COINS' - }; - - const response = await request(app) - .post('/api/v1/transactions') - .set('x-api-key', 'test-key') - .send(transaction) - .expect(400); - - expect(response.body.error.name).toBe('validationError'); - }); - }); -}); - diff --git a/tests/integration/workflows.test.js b/tests/integration/workflows.test.js deleted file mode 100644 index 10fb10c..0000000 --- a/tests/integration/workflows.test.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * End-to-End Workflow Integration Tests - * Tests complete user scenarios across multiple endpoints - */ - -const request = require('supertest'); -const express = require('express'); -const adminRoutes = require('../../src/routes/admin'); -const accountRoutes = require('../../src/routes/accounts'); -const transactionRoutes = require('../../src/routes/transactions'); - -// Mock auth middleware for testing -jest.mock('../../src/middleware/auth', () => ({ - validateApiKey: (req, res, next) => { - req.apiKey = req.headers['x-api-key'] || 'test-key'; - next(); - }, - requireAdmin: (req, res, next) => { - if (req.apiKey === 'admin-key') { - next(); - } else { - res.status(403).json({ - error: { - name: 'forbiddenError', - message: 'You do not have permissions to perform this action. Admin access required.' - } - }); - } - } -})); - -const db = require('../../src/database/db'); - -// Create fresh database for each test -beforeEach(() => { - db.accounts.clear(); - db.transactions.clear(); - db.apiKeys.clear(); - db.apiKeys.add('1234'); // Re-add default API key - db.initializeSampleData(); -}); - -// Create test app with all routes -const app = express(); -app.use(express.json()); -app.use('/api/v1', adminRoutes); -app.use('/api/v1/accounts', accountRoutes); -app.use('/api/v1/transactions', transactionRoutes); - -describe('End-to-End Workflows', () => { - describe('Error Handling Workflow', () => { - test('should handle currency mismatch across workflow', async () => { - const apiKey = 'test-key'; - - // Account 3 uses GALAXY_GOLD, try to transfer COSMIC_COINS - await request(app) - .post('/api/v1/transactions') - .set('x-api-key', apiKey) - .send({ - fromAccountId: '1', - toAccountId: '3', - amount: 100, - currency: 'COSMIC_COINS' - }) - .expect(400); - }); - }); - - describe('Query and Filter Workflow', () => { - test('should filter and query data across multiple endpoints', async () => { - const apiKey = 'test-key'; - - // Create multiple transactions - await request(app) - .post('/api/v1/transactions') - .set('x-api-key', apiKey) - .send({ - fromAccountId: '1', - toAccountId: '2', - amount: 100, - currency: 'COSMIC_COINS' - }); - - await request(app) - .post('/api/v1/transactions') - .set('x-api-key', apiKey) - .send({ - fromAccountId: '1', - toAccountId: '3', - amount: 200, - currency: 'COSMIC_COINS' - }); - - // Filter transactions by source account - const fromAccount1 = await request(app) - .get('/api/v1/transactions?fromAccountId=1') - .set('x-api-key', apiKey) - .expect(200); - - expect(fromAccount1.body.transactions.length).toBeGreaterThanOrEqual(2); - - // Filter accounts by date - const accountsByDate = await request(app) - .get('/api/v1/accounts?createdAt=2023-04-10') - .set('x-api-key', apiKey) - .expect(200); - - expect(accountsByDate.body.accounts.length).toBeGreaterThan(0); - - // Filter accounts by owner - const accountsByOwner = await request(app) - .get('/api/v1/accounts?owner=Nova') - .set('x-api-key', apiKey) - .expect(200); - - expect(accountsByOwner.body.accounts.length).toBeGreaterThan(0); - }); - }); -}); - diff --git a/tests/unit/middleware/auth.test.js b/tests/unit/middleware/auth.test.js deleted file mode 100644 index f39fa66..0000000 --- a/tests/unit/middleware/auth.test.js +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Authentication Middleware Tests - */ - -const { validateApiKey, requireAdmin } = require('../../../src/middleware/auth'); - -// Mock the database -jest.mock('../../../src/database/db', () => ({ - validateApiKey: jest.fn() -})); - -const db = require('../../../src/database/db'); - -describe('Authentication Middleware', () => { - let req, res, next; - - beforeEach(() => { - req = { - headers: {} - }; - res = { - status: jest.fn().mockReturnThis(), - json: jest.fn() - }; - next = jest.fn(); - jest.clearAllMocks(); - }); - - describe('validateApiKey()', () => { - test('should accept valid API key from x-api-key header', () => { - req.headers['x-api-key'] = 'valid-key'; - db.validateApiKey.mockReturnValue(true); - - validateApiKey(req, res, next); - - expect(db.validateApiKey).toHaveBeenCalledWith('valid-key'); - expect(req.apiKey).toBe('valid-key'); - expect(next).toHaveBeenCalled(); - expect(res.status).not.toHaveBeenCalled(); - }); - - test('should accept valid API key from api-key header', () => { - req.headers['api-key'] = 'valid-key'; - db.validateApiKey.mockReturnValue(true); - - validateApiKey(req, res, next); - - expect(db.validateApiKey).toHaveBeenCalledWith('valid-key'); - expect(req.apiKey).toBe('valid-key'); - expect(next).toHaveBeenCalled(); - }); - - test('should reject request with missing API key', () => { - validateApiKey(req, res, next); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - error: { - name: 'authenticationError', - message: expect.stringContaining('API key is required') - } - }); - expect(next).not.toHaveBeenCalled(); - }); - - test('should reject request with invalid API key', () => { - req.headers['x-api-key'] = 'invalid-key'; - db.validateApiKey.mockReturnValue(false); - - validateApiKey(req, res, next); - - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ - error: { - name: 'authenticationError', - message: expect.stringContaining('Invalid API key') - } - }); - expect(next).not.toHaveBeenCalled(); - }); - }); - - describe('requireAdmin()', () => { - test('should allow admin with correct API key', () => { - req.apiKey = process.env.ADMIN_API_KEY || '1234'; - - requireAdmin(req, res, next); - - expect(next).toHaveBeenCalled(); - expect(res.status).not.toHaveBeenCalled(); - }); - - test('should reject non-admin user', () => { - req.apiKey = 'regular-user-key'; - - requireAdmin(req, res, next); - - expect(res.status).toHaveBeenCalledWith(403); - expect(res.json).toHaveBeenCalledWith({ - error: { - name: 'forbiddenError', - message: expect.stringContaining('do not have permissions') - } - }); - expect(next).not.toHaveBeenCalled(); - }); - - test('should use default admin key if env variable not set', () => { - const originalAdminKey = process.env.ADMIN_API_KEY; - delete process.env.ADMIN_API_KEY; - - req.apiKey = '1234'; - - requireAdmin(req, res, next); - - expect(next).toHaveBeenCalled(); - - // Restore - process.env.ADMIN_API_KEY = originalAdminKey; - }); - }); -}); - diff --git a/tests/unit/models/Account.test.js b/tests/unit/models/Account.test.js deleted file mode 100644 index afa69c9..0000000 --- a/tests/unit/models/Account.test.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * Account Model Tests - */ - -const Account = require('../../../src/models/Account'); - -describe('Account Model', () => { - describe('Constructor', () => { - test('should create an account with all properties', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS', '2024-01-10'); - - expect(account.accountId).toBe('1'); - expect(account.owner).toBe('John Doe'); - expect(account.balance).toBe(1000); - expect(account.currency).toBe('COSMIC_COINS'); - expect(account.createdAt).toBe('2024-01-10'); - }); - - test('should create an account with default balance', () => { - const account = new Account('1', 'John Doe', undefined, 'COSMIC_COINS'); - - expect(account.balance).toBe(0); - }); - - test('should create an account with default createdAt date', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS'); - - expect(account.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}$/); - }); - }); - - describe('validate()', () => { - test('should validate a correct account', () => { - const data = { - owner: 'John Doe', - balance: 1000, - currency: 'COSMIC_COINS' - }; - - const result = Account.validate(data); - - expect(result.isValid).toBe(true); - }); - - test('should reject missing owner', () => { - const data = { - balance: 1000, - currency: 'COSMIC_COINS' - }; - - const result = Account.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('Owner'); - }); - - test('should reject non-string owner', () => { - const data = { - owner: 123, - balance: 1000, - currency: 'COSMIC_COINS' - }; - - const result = Account.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('string'); - }); - - test('should reject missing currency', () => { - const data = { - owner: 'John Doe', - balance: 1000 - }; - - const result = Account.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('Currency'); - }); - - test('should reject invalid currency', () => { - const data = { - owner: 'John Doe', - balance: 1000, - currency: 'INVALID_CURRENCY' - }; - - const result = Account.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('Currency'); - }); - - test('should accept valid currencies', () => { - const currencies = ['COSMIC_COINS', 'GALAXY_GOLD', 'MOON_BUCKS']; - - currencies.forEach(currency => { - const data = { - owner: 'John Doe', - currency: currency - }; - - const result = Account.validate(data); - expect(result.isValid).toBe(true); - }); - }); - - test('should reject negative balance', () => { - const data = { - owner: 'John Doe', - balance: -100, - currency: 'COSMIC_COINS' - }; - - const result = Account.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('Balance'); - }); - - test('should reject non-number balance', () => { - const data = { - owner: 'John Doe', - balance: '1000', - currency: 'COSMIC_COINS' - }; - - const result = Account.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('number'); - }); - - test('should accept zero balance', () => { - const data = { - owner: 'John Doe', - balance: 0, - currency: 'COSMIC_COINS' - }; - - const result = Account.validate(data); - - expect(result.isValid).toBe(true); - }); - }); - - describe('updateBalance()', () => { - test('should add to balance with positive amount', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS'); - - account.updateBalance(500); - - expect(account.balance).toBe(1500); - }); - - test('should subtract from balance with negative amount', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS'); - - account.updateBalance(-300); - - expect(account.balance).toBe(700); - }); - - test('should handle zero amount', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS'); - - account.updateBalance(0); - - expect(account.balance).toBe(1000); - }); - }); - - describe('hasSufficientFunds()', () => { - test('should return true when balance is sufficient', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS'); - - expect(account.hasSufficientFunds(500)).toBe(true); - }); - - test('should return true when amount equals balance', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS'); - - expect(account.hasSufficientFunds(1000)).toBe(true); - }); - - test('should return false when balance is insufficient', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS'); - - expect(account.hasSufficientFunds(1500)).toBe(false); - }); - - test('should return true for zero amount', () => { - const account = new Account('1', 'John Doe', 0, 'COSMIC_COINS'); - - expect(account.hasSufficientFunds(0)).toBe(true); - }); - }); - - describe('toJSON()', () => { - test('should return correct JSON representation', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS', '2024-01-10'); - - const json = account.toJSON(); - - expect(json).toEqual({ - accountId: '1', - owner: 'John Doe', - createdAt: '2024-01-10', - balance: 1000, - currency: 'COSMIC_COINS' - }); - }); - - test('should include all required properties', () => { - const account = new Account('1', 'John Doe', 1000, 'COSMIC_COINS'); - - const json = account.toJSON(); - - expect(json).toHaveProperty('accountId'); - expect(json).toHaveProperty('owner'); - expect(json).toHaveProperty('createdAt'); - expect(json).toHaveProperty('balance'); - expect(json).toHaveProperty('currency'); - }); - }); -}); - diff --git a/tests/unit/models/Book.test.js b/tests/unit/models/Book.test.js new file mode 100644 index 0000000..53aa7a0 --- /dev/null +++ b/tests/unit/models/Book.test.js @@ -0,0 +1,25 @@ +const Book = require('../../../src/models/Book'); + +describe('Book model', () => { + test('validate accepts valid data', () => { + expect(Book.validate({ title: 'Dune', author: 'Frank Herbert', year: 1965 })).toEqual({ isValid: true }); + expect(Book.validate({ title: 'X', author: 'Y' })).toEqual({ isValid: true }); + }); + + test('validate rejects missing title', () => { + const r = Book.validate({ author: 'Someone' }); + expect(r.isValid).toBe(false); + expect(r.error).toContain('Title'); + }); + + test('validate rejects missing author', () => { + const r = Book.validate({ title: 'Something' }); + expect(r.isValid).toBe(false); + expect(r.error).toContain('Author'); + }); + + test('toJSON returns plain object', () => { + const book = new Book('1', 'Dune', 'Frank Herbert', 1965); + expect(book.toJSON()).toEqual({ id: '1', title: 'Dune', author: 'Frank Herbert', year: 1965 }); + }); +}); diff --git a/tests/unit/models/Transaction.test.js b/tests/unit/models/Transaction.test.js deleted file mode 100644 index 4ded7fb..0000000 --- a/tests/unit/models/Transaction.test.js +++ /dev/null @@ -1,231 +0,0 @@ -/** - * Transaction Model Tests - */ - -const Transaction = require('../../../src/models/Transaction'); - -describe('Transaction Model', () => { - describe('Constructor', () => { - test('should create a transaction with all properties', () => { - const transaction = new Transaction( - '1', - 'acc1', - 'acc2', - 1000, - 'COSMIC_COINS', - '2024-01-10' - ); - - expect(transaction.transactionId).toBe('1'); - expect(transaction.fromAccountId).toBe('acc1'); - expect(transaction.toAccountId).toBe('acc2'); - expect(transaction.amount).toBe(1000); - expect(transaction.currency).toBe('COSMIC_COINS'); - expect(transaction.createdAt).toBe('2024-01-10'); - }); - - test('should create a transaction with default createdAt', () => { - const transaction = new Transaction( - '1', - 'acc1', - 'acc2', - 1000, - 'COSMIC_COINS' - ); - - expect(transaction.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}$/); - }); - }); - - describe('validate()', () => { - test('should validate a correct transaction', () => { - const data = { - fromAccountId: 'acc1', - toAccountId: 'acc2', - amount: 1000, - currency: 'COSMIC_COINS' - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(true); - }); - - test('should validate a deposit transaction (fromAccountId = "0")', () => { - const data = { - fromAccountId: '0', - toAccountId: 'acc2', - amount: 1000, - currency: 'COSMIC_COINS' - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(true); - }); - - test('should reject missing fromAccountId', () => { - const data = { - toAccountId: 'acc2', - amount: 1000, - currency: 'COSMIC_COINS' - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('fromAccountId'); - }); - - test('should reject missing toAccountId', () => { - const data = { - fromAccountId: 'acc1', - amount: 1000, - currency: 'COSMIC_COINS' - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('toAccountId'); - }); - - test('should reject missing amount', () => { - const data = { - fromAccountId: 'acc1', - toAccountId: 'acc2', - currency: 'COSMIC_COINS' - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('Amount'); - }); - - test('should reject zero amount', () => { - const data = { - fromAccountId: 'acc1', - toAccountId: 'acc2', - amount: 0, - currency: 'COSMIC_COINS' - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('positive'); - }); - - test('should reject negative amount', () => { - const data = { - fromAccountId: 'acc1', - toAccountId: 'acc2', - amount: -500, - currency: 'COSMIC_COINS' - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('positive'); - }); - - test('should reject non-number amount', () => { - const data = { - fromAccountId: 'acc1', - toAccountId: 'acc2', - amount: '1000', - currency: 'COSMIC_COINS' - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('number'); - }); - - test('should reject missing currency', () => { - const data = { - fromAccountId: 'acc1', - toAccountId: 'acc2', - amount: 1000 - }; - - const result = Transaction.validate(data); - - expect(result.isValid).toBe(false); - expect(result.error).toContain('Currency'); - }); - }); - - describe('isDeposit()', () => { - test('should return true for deposit transactions', () => { - const transaction = new Transaction( - '1', - '0', - 'acc2', - 1000, - 'COSMIC_COINS' - ); - - expect(transaction.isDeposit()).toBe(true); - }); - - test('should return false for transfer transactions', () => { - const transaction = new Transaction( - '1', - 'acc1', - 'acc2', - 1000, - 'COSMIC_COINS' - ); - - expect(transaction.isDeposit()).toBe(false); - }); - }); - - describe('toJSON()', () => { - test('should return correct JSON representation', () => { - const transaction = new Transaction( - '1', - 'acc1', - 'acc2', - 1000, - 'COSMIC_COINS', - '2024-01-10' - ); - - const json = transaction.toJSON(); - - expect(json).toEqual({ - transactionId: '1', - createdAt: '2024-01-10', - amount: 1000, - currency: 'COSMIC_COINS', - fromAccountId: 'acc1', - toAccountId: 'acc2' - }); - }); - - test('should include all required properties', () => { - const transaction = new Transaction( - '1', - 'acc1', - 'acc2', - 1000, - 'COSMIC_COINS' - ); - - const json = transaction.toJSON(); - - expect(json).toHaveProperty('transactionId'); - expect(json).toHaveProperty('createdAt'); - expect(json).toHaveProperty('amount'); - expect(json).toHaveProperty('currency'); - expect(json).toHaveProperty('fromAccountId'); - expect(json).toHaveProperty('toAccountId'); - }); - }); -}); - From 1d318bfa77bf643abea8e62b3b6d3763168f0942 Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 16:25:01 +0100 Subject: [PATCH 7/9] fix: fixed collection name in workflow --- .github/workflows/postman.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/postman.yaml b/.github/workflows/postman.yaml index 0062f08..1134435 100644 --- a/.github/workflows/postman.yaml +++ b/.github/workflows/postman.yaml @@ -38,12 +38,12 @@ jobs: sleep 2 done - - name: Validate Collection and Spec + - name: Validate Collection and Push to Cloud env: POSTMAN_API_KEY: ${{ secrets.POSTMAN_API_KEY }} run: | postman login --with-api-key "$POSTMAN_API_KEY" - postman collection run ./postman/collections/Intergalactic-Bank-API + postman collection run ./postman/collections/Book-API postman workspace prepare postman workspace push -y From e6a371a7f2e1641a57fc1a37b7029492ab024873 Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 16:52:29 +0100 Subject: [PATCH 8/9] fix: fixed collection name in workflow --- .github/workflows/postman.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/postman.yaml b/.github/workflows/postman.yaml index 1134435..d294bbb 100644 --- a/.github/workflows/postman.yaml +++ b/.github/workflows/postman.yaml @@ -44,7 +44,6 @@ jobs: run: | postman login --with-api-key "$POSTMAN_API_KEY" postman collection run ./postman/collections/Book-API - postman workspace prepare postman workspace push -y - name: Stop server From f1df1c2ec5d011e047e39231fe5716af04e8a73d Mon Sep 17 00:00:00 2001 From: Gbadebo Bello Date: Tue, 3 Mar 2026 16:58:29 +0100 Subject: [PATCH 9/9] fix --- .github/workflows/postman.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/postman.yaml b/.github/workflows/postman.yaml index d294bbb..1134435 100644 --- a/.github/workflows/postman.yaml +++ b/.github/workflows/postman.yaml @@ -44,6 +44,7 @@ jobs: run: | postman login --with-api-key "$POSTMAN_API_KEY" postman collection run ./postman/collections/Book-API + postman workspace prepare postman workspace push -y - name: Stop server