diff --git a/CHANGELOG.md b/CHANGELOG.md index f3ee8e13..8173bf47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## [1.42.2](https://github.com/Greenstand/treetracker-wallet-api/compare/v1.42.1...v1.42.2) (2025-04-17) + ## [1.43.1-keycloak.2](https://github.com/Greenstand/treetracker-wallet-api/compare/v1.43.1-keycloak.1...v1.43.1-keycloak.2) (2025-04-15) @@ -6,6 +8,9 @@ * env variable ([144a446](https://github.com/Greenstand/treetracker-wallet-api/commit/144a4469ee99fef5a2252fa2d8b8eb531ea7120a)) ## [1.43.1-keycloak.1](https://github.com/Greenstand/treetracker-wallet-api/compare/v1.43.0...v1.43.1-keycloak.1) (2025-04-15) +* s3 url ([c642005](https://github.com/Greenstand/treetracker-wallet-api/commit/c6420057616afcf77f3c99ce812e7791a2fef10d)) + +## [1.42.1](https://github.com/Greenstand/treetracker-wallet-api/compare/v1.42.0...v1.42.1) (2025-04-17) ### Bug Fixes @@ -18,6 +23,9 @@ ### Bug Fixes * fix integration tests ([56217c1](https://github.com/Greenstand/treetracker-wallet-api/commit/56217c1c43eaa976bd1e718cedd553cb7e42ea91)) +* s3 url ([5b4aa80](https://github.com/Greenstand/treetracker-wallet-api/commit/5b4aa804ff112030126c99ac103b45c90d91cbb8)) + +# [1.42.0](https://github.com/Greenstand/treetracker-wallet-api/compare/v1.41.1...v1.42.0) (2025-02-18) ### Features diff --git a/__tests__/wallet-get.spec.js b/__tests__/wallet-get.spec.js index 9617696a..a730006d 100644 --- a/__tests__/wallet-get.spec.js +++ b/__tests__/wallet-get.spec.js @@ -2,6 +2,7 @@ require('dotenv').config(); const request = require('supertest'); const { expect } = require('chai'); const chai = require('chai'); +const uuid = require('uuid'); const server = require('../server/app'); const seed = require('./seed'); chai.use(require('chai-uuid')); @@ -34,6 +35,15 @@ describe('Wallet: Get wallets of an account', () => { expect(res.count).to.eq(11); }); + it('should return 401, user does not exist', async () => { + await request(server) + .get('/wallets') + .send({ wallet: 'azAZ.-@0123456789' }) + .set('content-type', 'application/json') + .set('Authorization', `Bearer ${uuid.v4()}`) + .expect(401); + }); + it('Get wallets of WalletA without params', async () => { const res = await request(server) .get('/wallets') diff --git a/__tests__/wallet-post.spec.js b/__tests__/wallet-post.spec.js index 6d64b4c3..e277e033 100644 --- a/__tests__/wallet-post.spec.js +++ b/__tests__/wallet-post.spec.js @@ -2,10 +2,12 @@ require('dotenv').config(); const request = require('supertest'); const { expect } = require('chai'); const chai = require('chai'); +const uuid = require('uuid'); const server = require('../server/app'); const seed = require('./seed'); chai.use(require('chai-uuid')); const WalletService = require('../server/services/WalletService'); +const TrustService = require('../server/services/TrustService'); describe('Wallet: create(POST) wallets of an account', () => { let bearerTokenA; @@ -17,19 +19,57 @@ describe('Wallet: create(POST) wallets of an account', () => { bearerTokenA = seed.wallet.keycloak_account_id; }); + it('create parent wallet', async () => { + const keycloakId = uuid.v4(); + const res = await request(server) + .post('/wallets') + .send({ wallet: 'newParentWallet' }) + .set('content-type', 'application/json') + .set('Authorization', `Bearer ${keycloakId}`) + .expect(201); + + expect(res.body).contain({ wallet: 'newParentWallet' }); + expect(res.body.id).to.exist; + + const walletService = new WalletService(); + const wallet = await walletService.getWalletIdByKeycloakId(keycloakId); + expect(wallet.id).equal(res.body.id); + + const trustService = new TrustService(); + const trust = await trustService.getTrustRelationships(res.body.id, [], { + walletId: res.body.id, + }); + expect(trust.count).equal(0); + expect(trust.result.length).equal(0); + }); + it('create wallet by a valid wallet name', async () => { const walletService = new WalletService(); const res = await request(server) .post('/wallets') .send({ wallet: 'azAZ.-@0123456789' }) .set('content-type', 'application/json') - .set('Authorization', `Bearer ${bearerTokenA}`); + .set('Authorization', `Bearer ${bearerTokenA}`) + .expect(201); expect(res.body).contain({ wallet: 'azAZ.-@0123456789' }); expect(res.body.id).to.exist; await walletService.getById(res.body.id).then((wallet) => { expect(wallet.name).to.eq('azAZ.-@0123456789'); }); + + const trustService = new TrustService(); + const trust = await trustService.getTrustRelationships(res.body.id, [], { + walletId: res.body.id, + }); + expect(trust.count).equal(1); + expect(trust.result.length).equal(1); + expect(trust.result[0].originating_wallet).equal('walletA'); + expect(trust.result[0].actor_wallet).equal('walletA'); + expect(trust.result[0].target_wallet).equal('azAZ.-@0123456789'); + expect(trust.result[0].type).equal('manage'); + expect(trust.result[0].state).equal('trusted'); + expect(trust.result[0].request_type).equal('manage'); }); it('create wallet by invalid name length', async () => { diff --git a/server/handlers/trustHandler/index.js b/server/handlers/trustHandler/index.js index 9fafb086..18398855 100644 --- a/server/handlers/trustHandler/index.js +++ b/server/handlers/trustHandler/index.js @@ -5,6 +5,9 @@ const { trustGetQuerySchema, } = require('./schemas'); +// @TODO(deprecated) is this endpoint still in use +// /wallet/:id/trust_relationships works better +// if not in use remove and remove the getAllTrustRelationships function const trustGet = async (req, res) => { const validatedQuery = await trustGetQuerySchema.validateAsync(req.query, { abortEarly: false, diff --git a/server/handlers/walletHandler.spec.js b/server/handlers/walletHandler.spec.js index d41d9f9b..0482870e 100644 --- a/server/handlers/walletHandler.spec.js +++ b/server/handlers/walletHandler.spec.js @@ -194,6 +194,34 @@ describe('walletRouter', () => { ).eql(true); }); + it('successfully creates parent wallet', async () => { + sinon.restore(); + sinon.stub(JWTService, 'verify').returns({ + id: keycloakId, + }); + sinon + .stub(WalletService.prototype, 'getWalletIdByKeycloakId') + .resolves({}); + const createParentWalletStub = sinon + .stub(WalletService.prototype, 'createParentWallet') + .resolves(mockWallet); + const res = await request(app).post('/wallets').send({ + wallet: mockWallet.wallet, + about: mockWallet.about, + }); + expect(res).property('statusCode').eq(201); + expect(res.body.wallet).eq(mockWallet.wallet); + expect(res.body.id).eq(mockWallet.id); + expect(res.body.about).eq(mockWallet.about); + expect( + createParentWalletStub.calledOnceWithExactly( + keycloakId, + mockWallet.wallet, + mockWallet.about, + ), + ).eql(true); + }); + it('missed parameter', async () => { const res = await request(app).post('/wallets').send({}); expect(res).property('statusCode').eq(422); diff --git a/server/handlers/walletHandler/index.js b/server/handlers/walletHandler/index.js index 3d8b0abb..4f791f71 100644 --- a/server/handlers/walletHandler/index.js +++ b/server/handlers/walletHandler/index.js @@ -14,6 +14,7 @@ const { walletBatchTransferBodySchema, csvValidationSchemaTransfer, } = require('./schemas'); +const HttpError = require('../../utils/HttpError'); const walletGet = async (req, res) => { const validatedQuery = await walletGetQuerySchema.validateAsync(req.query, { @@ -141,11 +142,24 @@ const walletPost = async (req, res) => { const { wallet_id } = req; const { wallet: walletToBeCreated, about } = validatedBody; const walletService = new WalletService(); - const returnedWallet = await walletService.createWallet( - wallet_id, - walletToBeCreated, - about, - ); + + let returnedWallet; + if (!wallet_id) { + // new keycloak user + const { keycloak_id } = req; + if (!keycloak_id) throw new HttpError(500, 'keycloak id not found'); + returnedWallet = await walletService.createParentWallet( + keycloak_id, + walletToBeCreated, + about, + ); + } else { + returnedWallet = await walletService.createWallet( + wallet_id, + walletToBeCreated, + about, + ); + } res.status(201).json(returnedWallet); }; diff --git a/server/models/Wallet.js b/server/models/Wallet.js index e165ea08..7c6d7ca7 100644 --- a/server/models/Wallet.js +++ b/server/models/Wallet.js @@ -13,8 +13,7 @@ class Wallet { this._tokenRepository = new TokenRepository(session); } - async createWallet(loggedInWalletId, wallet, about) { - // check name + async checkWalletName(wallet) { try { await this._walletRepository.getByName(wallet); throw new HttpError(409, `The wallet "${wallet}" already exists`); @@ -25,8 +24,24 @@ class Wallet { throw e; } } + } + + async createParentWallet(keycloakId, wallet, about) { + await this.checkWalletName(wallet); + + const newWallet = await this._walletRepository.create({ + name: wallet, + about, + keycloak_account_id: keycloakId, + }); + + return newWallet; + } + + async createWallet(loggedInWalletId, wallet, about) { + // check name + await this.checkWalletName(wallet); - // TO DO: check if wallet is expected format type? // TO DO: Need to check account permissions -> manage accounts // need to create a wallet object diff --git a/server/models/Wallet.spec.js b/server/models/Wallet.spec.js index 60703943..9c6a1bd7 100644 --- a/server/models/Wallet.spec.js +++ b/server/models/Wallet.spec.js @@ -31,7 +31,7 @@ describe('Wallet Model', () => { sinon.restore(); }); - describe('createWallet function', async () => { + describe('createWallet function', () => { const walletId = uuid(); const wallet = 'wallet'; @@ -94,6 +94,67 @@ describe('Wallet Model', () => { }); }); + describe('createParentWallet function', () => { + const wallet = 'wallet'; + + it('should error out, wallet already exists', async () => { + walletRepositoryStub.getByName.resolves(); + let error; + + try { + await walletModel.createParentWallet(uuid(), wallet); + } catch (e) { + error = e; + } + + expect(error.code).eql(409); + expect(error.message).eql(`The wallet "wallet" already exists`); + expect(walletRepositoryStub.getByName).calledOnceWithExactly('wallet'); + expect(trustRepositoryStub.create).not.called; + expect(walletRepositoryStub.create).not.called; + }); + + it('should error out, some other error', async () => { + walletRepositoryStub.getByName.rejects('internal error'); + let error; + + try { + await walletModel.createParentWallet(uuid(), wallet); + } catch (e) { + error = e; + } + + expect(error.toString()).eql(`internal error`); + expect(walletRepositoryStub.getByName).calledOnceWithExactly('wallet'); + expect(trustRepositoryStub.create).not.called; + expect(walletRepositoryStub.create).not.called; + }); + + it('should create default wallet', async () => { + walletRepositoryStub.getByName.rejects(new HttpError(404)); + const newWalletId = uuid(); + const keycloakId = uuid(); + const about = 'default wallet about'; + + walletRepositoryStub.create.resolves({ id: newWalletId }); + + const result = await walletModel.createParentWallet( + keycloakId, + wallet, + about, + ); + + expect(result).eql({ id: newWalletId }); + expect(trustRepositoryStub.create).not.called; + expect(walletRepositoryStub.getByName).calledOnceWithExactly('wallet'); + expect(walletRepositoryStub.create).calledOnceWithExactly({ + name: wallet, + about, + keycloak_account_id: keycloakId, + }); + }); + }); + it('getById function', async () => { const walletId = uuid(); walletRepositoryStub.getById.resolves({ id: walletId, wallet: 'wallet' }); diff --git a/server/repositories/WalletRepository.js b/server/repositories/WalletRepository.js index bcd7849f..38351ccc 100644 --- a/server/repositories/WalletRepository.js +++ b/server/repositories/WalletRepository.js @@ -62,9 +62,7 @@ class WalletRepository extends BaseRepository { .table(this._tableName) .where('keycloak_account_id', keycloakAccountId) .first(); - if (!object) { - throw new HttpError(404, `User is not associated with any wallet`); - } + return object; } diff --git a/server/services/S3Service.js b/server/services/S3Service.js index 3821d466..b3134fd7 100644 --- a/server/services/S3Service.js +++ b/server/services/S3Service.js @@ -14,7 +14,7 @@ const upload = async (file, key, mimetype) => { const command = new PutObjectCommand(params); await s3.send(command); - return `https://${bucket}.s3-${region}.amazonaws.com/${key}`; + return `https://${bucket}.s3.${region}.amazonaws.com/${encodeURIComponent(key)}`; }; module.exports = { diff --git a/server/services/WalletService.js b/server/services/WalletService.js index 246f5385..cc0d60c3 100644 --- a/server/services/WalletService.js +++ b/server/services/WalletService.js @@ -48,6 +48,20 @@ class WalletService { return wallet; } + async createParentWallet(keycloakId, wallet, about) { + const newParentWallet = await this._wallet.createParentWallet( + keycloakId, + wallet, + about, + ); + + return { + id: newParentWallet.id, + wallet: newParentWallet.name, + about: newParentWallet.about, + }; + } + async createWallet(loggedInWalletId, wallet, about) { try { await this._session.beginTransaction(); diff --git a/server/services/WalletService.spec.js b/server/services/WalletService.spec.js index c26e49c6..d7202550 100644 --- a/server/services/WalletService.spec.js +++ b/server/services/WalletService.spec.js @@ -18,6 +18,28 @@ describe('WalletService', () => { sinon.restore(); }); + it('createParentWallet', async () => { + const createParentWalletStub = sinon.stub( + Wallet.prototype, + 'createParentWallet', + ); + const keycloakId = uuid.v4(); + const wallet = 'wallet'; + const about = 'test about'; + const walletId = uuid.v4(); + createParentWalletStub.resolves({ name: wallet, id: walletId, about }); + expect(walletService).instanceOf(WalletService); + const createdParentWallet = await walletService.createParentWallet( + keycloakId, + wallet, + about, + ); + expect(createdParentWallet).eql({ wallet, id: walletId, about }); + expect( + createParentWalletStub.calledOnceWithExactly(keycloakId, wallet, about), + ).eql(true); + }); + it('getById', async () => { const walletId1 = uuid.v4(); sinon diff --git a/server/utils/utils.js b/server/utils/utils.js index c07e6ba7..73d6a04b 100644 --- a/server/utils/utils.js +++ b/server/utils/utils.js @@ -54,13 +54,18 @@ exports.errorHandler = (err, req, res, _next) => { exports.verifyJWTHandler = exports.handlerWrapper(async (req, res, next) => { const result = JWTService.verify(req.headers.authorization); const walletService = new WalletService(); - // there is a 404 error check in the repository - try { - const wallet = await walletService.getWalletIdByKeycloakId(result.id); + + const wallet = await walletService.getWalletIdByKeycloakId(result.id); + if (!wallet || !wallet.id) { + if (req.originalUrl === '/wallets' && req.method === 'POST') { + req.keycloak_id = result.id; + next(); + } else { + log.error('user info not found'); + throw new HttpError(401, 'ERROR: Authentication, invalid token received'); + } + } else { req.wallet_id = wallet.id; next(); - } catch (e) { - log.error('user info not found'); - throw new HttpError(401, 'ERROR: Authentication, invalid token received'); } }); diff --git a/server/utils/utils.spec.js b/server/utils/utils.spec.js index f09b7f88..e68d32d0 100644 --- a/server/utils/utils.spec.js +++ b/server/utils/utils.spec.js @@ -122,6 +122,37 @@ describe('routers/utils', () => { sinon.restore(); }); + it('to create parent wallet', async () => { + const keycloakId = uuid.v4(); + const verifyStub = sinon.stub(JWTService, 'verify').returns({ + id: keycloakId, + }); + const getWalletIdByKeycloakId = sinon + .stub(WalletService.prototype, 'getWalletIdByKeycloakId') + .resolves(); + const app = express(); + // mock + app.post('/wallets', [ + helper.verifyJWTHandler, + async (req, res) => + res.status(200).send({ keycloakId: req.keycloak_id }), + ]); + app.use(helper.errorHandler); + + const res = await request(app) + .post('/wallets') + .set('Authorization', `Bearer token`); + expect(res.statusCode).eq(200); + expect(res.body).eql({ + keycloakId, + }); + expect(getWalletIdByKeycloakId.calledOnceWithExactly(keycloakId)).eql( + true, + ); + expect(verifyStub.calledOnceWithExactly('Bearer token')).eql(true); + sinon.restore(); + }); + it('pass corupt token should get response with code 403', async () => { const keycloakId = uuid.v4(); const verifyStub = sinon.stub(JWTService, 'verify').returns({ @@ -129,17 +160,17 @@ describe('routers/utils', () => { }); const getWalletIdByKeycloakId = sinon .stub(WalletService.prototype, 'getWalletIdByKeycloakId') - .rejects(); + .resolves(); const app = express(); // mock - app.get('/test', [ + app.get('/wallets', [ helper.verifyJWTHandler, - async (req, res) => res.status(200).send({ id: req.walletId }), + async (req, res) => res.status(200).send({ id: req.wallet_id }), ]); app.use(helper.errorHandler); const res = await request(app) - .get('/test') + .get('/wallets') .set('Authorization', `Bearer token`); expect(res.statusCode).eq(401); expect(res.body).eql({