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
81 changes: 81 additions & 0 deletions __tests__/actiontoken-redeem-wallet.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require('dotenv').config();
const request = require('supertest');
const { expect } = require('chai');
const server = require('../server/app');
const seed = require('./seed');

describe('Action token: claiming into a chosen wallet', () => {
let bearerTokenA;
let bearerTokenB;
let link;

beforeEach(async () => {
await seed.clear();
await seed.seed();
bearerTokenA = seed.wallet.keycloak_account_id;
bearerTokenB = seed.walletB.keycloak_account_id;

const issued = await request(server)
.post('/action-tokens')
.set('Authorization', `Bearer ${bearerTokenA}`)
.send({
recipient_email: 'samwel@example.com',
bundle: { bundle_size: 1 },
})
.expect(201);
link = issued.body.action_token;
});

it('accepts a wallet name, which the app sends on every claim', async () => {
const res = await request(server)
.post('/action-tokens/redeem')
.set('Authorization', `Bearer ${bearerTokenB}`)
.send({ action_token: link, wallet: seed.walletB.name });

expect(res).to.have.property('statusCode', 200);
expect(res.body).to.have.property('state', 'completed');
});

it('puts the tokens in the wallet just created, not the login wallet', async () => {
const created = await request(server)
.post('/wallets')
.set('content-type', 'application/json')
.set('Authorization', `Bearer ${bearerTokenB}`)
.send({ wallet: 'walletB-second' })
.expect(201);

const redeemed = await request(server)
.post('/action-tokens/redeem')
.set('Authorization', `Bearer ${bearerTokenB}`)
.send({ action_token: link, wallet: 'walletB-second' })
.expect(200);

const moved = redeemed.body.parameters.tokens[0];

const inNew = await request(server)
.get(`/tokens?wallet=walletB-second`)
.set('Authorization', `Bearer ${bearerTokenB}`)
.expect(200);

expect(created.body.id).to.exist;
expect(inNew.body.tokens.map((t) => t.id)).to.include(moved);
});

it('still claims into the login wallet when no wallet is named', async () => {
const res = await request(server)
.post('/action-tokens/redeem')
.set('Authorization', `Bearer ${bearerTokenB}`)
.send({ action_token: link });

expect(res).to.have.property('statusCode', 200);
});

it('refuses a wallet the account does not own', async () => {
const res = await request(server)
.post('/action-tokens/redeem')
.set('Authorization', `Bearer ${bearerTokenB}`)
.send({ action_token: link, wallet: seed.wallet.name });

expect(res).to.have.property('statusCode', 403);
});
});
8 changes: 6 additions & 2 deletions server/handlers/actionTokenHandler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ const actionTokenRedeem = async (req, res) => {
const validatedBody = await actionTokenRedeemSchema.validateAsync(req.body, {
abortEarly: false,
});
const { wallet_id } = req;
const { wallet_id, keycloak_id } = req;

const actionTokenService = new ActionTokenService();
const result = await actionTokenService.redeem(validatedBody, wallet_id);
const result = await actionTokenService.redeem(
validatedBody,
wallet_id,
keycloak_id,
);

res.status(200).json(result);
};
Expand Down
1 change: 1 addition & 0 deletions server/handlers/actionTokenHandler/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const actionTokenGenerateSchema = Joi.alternatives().conditional(

const actionTokenRedeemSchema = Joi.object({
action_token: Joi.string().required(),
wallet: Joi.string(),
});

module.exports = { actionTokenGenerateSchema, actionTokenRedeemSchema };
18 changes: 16 additions & 2 deletions server/services/ActionTokenService.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,25 @@ class ActionTokenService {
};
}

async redeem({ action_token }, walletLoginId) {
async redeem({ action_token, wallet }, walletLoginId, keycloakId) {
const payload = ActionTokenService.verifyActionToken(action_token);

let receiverWalletId = walletLoginId;
if (wallet) {
const receiver = await this._walletService.getByName(wallet);
const isOwn = await this._walletService.canActFor(receiver, {
keycloakId,
walletLoginId,
});
if (!isOwn) {
throw new HttpError(403, 'Wallet does not belong to this account');
}
receiverWalletId = receiver.id;
}

return this._transferService.redeemActionToken({
senderWalletId: payload.sender_wallet_id,
receiverWalletId: walletLoginId,
receiverWalletId,
tokenIds: payload.token_ids,
tokenCount: payload.token_count,
});
Expand Down
Loading