Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)


Expand All @@ -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
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions __tests__/wallet-get.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -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')
Expand Down
42 changes: 41 additions & 1 deletion __tests__/wallet-post.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 () => {
Expand Down
3 changes: 3 additions & 0 deletions server/handlers/trustHandler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions server/handlers/walletHandler.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
24 changes: 19 additions & 5 deletions server/handlers/walletHandler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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);
};
Expand Down
21 changes: 18 additions & 3 deletions server/models/Wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand All @@ -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
Expand Down
63 changes: 62 additions & 1 deletion server/models/Wallet.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('Wallet Model', () => {
sinon.restore();
});

describe('createWallet function', async () => {
describe('createWallet function', () => {
const walletId = uuid();
const wallet = 'wallet';

Expand Down Expand Up @@ -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' });
Expand Down
4 changes: 1 addition & 3 deletions server/repositories/WalletRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion server/services/S3Service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
14 changes: 14 additions & 0 deletions server/services/WalletService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions server/services/WalletService.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading