diff --git a/src/controllers/rest-api/p2wdb/controller.js b/src/controllers/rest-api/p2wdb/controller.js index 6660868..6f1348e 100644 --- a/src/controllers/rest-api/p2wdb/controller.js +++ b/src/controllers/rest-api/p2wdb/controller.js @@ -64,6 +64,7 @@ class P2WDBRESTControllerLib { await _this.useCases.offer.flagOffer(flagData) } else { console.log('Could not route P2WDB webhook data.') + // NOTE: Should we throw an error here? } // const orderObj = ctx.request.body.order diff --git a/src/controllers/rest-api/smAccount/controller.js b/src/controllers/rest-api/smAccount/controller.js index 0ac2fb6..17896af 100644 --- a/src/controllers/rest-api/smAccount/controller.js +++ b/src/controllers/rest-api/smAccount/controller.js @@ -12,13 +12,13 @@ class SmAccountRESTControllerLib { this.adapters = localConfig.adapters if (!this.adapters) { throw new Error( - 'Instance of Adapters library required when instantiating /order REST Controller.' + 'Instance of Adapters library required when instantiating /sm REST Controller.' ) } this.useCases = localConfig.useCases if (!this.useCases) { throw new Error( - 'Instance of Use Cases library required when instantiating /order REST Controller.' + 'Instance of Use Cases library required when instantiating /sm REST Controller.' ) } diff --git a/src/controllers/rest-api/smAccount/index.js b/src/controllers/rest-api/smAccount/index.js index 8420eca..52d6ac6 100644 --- a/src/controllers/rest-api/smAccount/index.js +++ b/src/controllers/rest-api/smAccount/index.js @@ -40,7 +40,6 @@ class SmAccountRouter { const baseUrl = '/sm' this.router = new Router({ prefix: baseUrl }) - this.listAccounts = this.listAccounts.bind(this) _this = this } @@ -60,11 +59,6 @@ class SmAccountRouter { app.use(_this.router.routes()) app.use(_this.router.allowedMethods()) } - - async listAccounts (ctx, next) { - // await _this.validators.ensureUser(ctx, next) - await _this.smAccountRESTController.listAccounts(ctx, next) - } } export default SmAccountRouter diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js index 6ece3b9..f34b258 100644 --- a/src/controllers/timer-controllers.js +++ b/src/controllers/timer-controllers.js @@ -105,6 +105,7 @@ class TimerControllers { console.error('Error in time-controller.js/cleanUsage(): ', err) this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval) + return false } } diff --git a/src/entities/entry.js b/src/entities/entry.js index adb5ebb..a732a04 100644 --- a/src/entities/entry.js +++ b/src/entities/entry.js @@ -1,6 +1,9 @@ /* Entry Entity + + TODO: Explain what an entry is and how it works. */ + class Entry { validate ({ entry, diff --git a/src/use-cases/entry.js b/src/use-cases/entry.js index 53aecde..5968836 100644 --- a/src/use-cases/entry.js +++ b/src/use-cases/entry.js @@ -1,3 +1,8 @@ +/* + Entry use-case library. + + TODO: Explain what an entry is and how it works. +*/ // local libraries import wlogger from '../adapters/wlogger.js' @@ -30,11 +35,12 @@ class EntryLib { if (!isValidSignature) { throw new Error('Invalid signature') } - // Verify psf tokens balance // const psfBalance = await this.bch.getPSFTokenBalance( // entryEntity.slpAddress // ) + // NOTE: this.bch.getPSFTokenBalance , does not exist , should be created? + const psfBalance = 1 if (psfBalance < 10) { diff --git a/src/use-cases/offer/index.js b/src/use-cases/offer/index.js index 9f8d250..2929f32 100644 --- a/src/use-cases/offer/index.js +++ b/src/use-cases/offer/index.js @@ -598,7 +598,9 @@ class OfferUseCases { throw new Error('Could not calculate the amount of BCH offered in the Counter Offer') } const satsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[2].value) + const hasRequiredAmount = satsOut === satsToReceive + if (!hasRequiredAmount) { throw new Error(`The Counter Offer has an output of ${satsOut}, which does not match the required ${satsToReceive} in the Offer.`) } @@ -615,25 +617,25 @@ class OfferUseCases { // Ensure the 3rd output (vout=2) is going to the maker address specified // in the Offer. + const addrInCounterOffer = txObj.vout[2].scriptPubKey.addresses[0] const makerAddr = orderData.makerAddr const hasCorrectAddr = makerAddr === addrInCounterOffer + if (!hasCorrectAddr) { throw new Error(`The Counter Offer has an output address of ${addrInCounterOffer}, which does not match the Maker address of ${makerAddr} in the Offer.`) } // Ensure the 4th output (vout=3) is going to the operator address specified in the Offer. + // NOTE: This is the operator of the bch-dex intance, NOT the seller of the token. const operatorAddr = orderData.operatorAddress + const hasCorrectOperatorAddr = operatorAddr === txObj.vout[3].scriptPubKey.addresses[0] if (!hasCorrectOperatorAddr) { throw new Error(`The Counter Offer has an output address of ${txObj.vout[3].scriptPubKey.addresses[0]}, which does not match the Operator address of ${operatorAddr} in the Offer.`) } // Ensure the 4th output (vout=3) contains the required amount of BCH. - const operatorSatsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit)) - if (isNaN(operatorSatsToReceive)) { - throw new Error('Could not calculate the amount of BCH offered in the Counter Offer') - } const operatorSatsOut = this.adapters.wallet.bchWallet.bchjs.BitcoinCash.toSatoshi(txObj.vout[3].value) let estimatedOperatorFee = Math.floor(txObj.vout[3].value * orderData.operatorPercentage / 100) if (estimatedOperatorFee < 546) estimatedOperatorFee = 546 @@ -814,7 +816,7 @@ class OfferUseCases { try { // Retrieve offers array. const offers = await this.adapters.nostr.read() - // console.log('offers: ', offers) + console.log('offers: ', offers) for (let i = 0; i < offers.length; i++) { try { @@ -825,7 +827,7 @@ class OfferUseCases { // Append the Nostr Event ID to the offer object offerObj.data.nostrEventId = offer.eventId - // console.log('loadOffers() offerObj: ', offerObj) + console.log('loadOffers() offerObj: ', offerObj) if (offerObj.data.dataType === 'offer') { // Try to create new offer diff --git a/src/use-cases/smAccount-use-cases.js b/src/use-cases/smAccount-use-cases.js index 36081e2..3b33d98 100644 --- a/src/use-cases/smAccount-use-cases.js +++ b/src/use-cases/smAccount-use-cases.js @@ -10,7 +10,7 @@ class smAccountUseCases { this.adapters = localConfig.adapters if (!this.adapters) { throw new Error( - 'Instance of adapters must be passed in when instantiating Order Use Cases library.' + 'Instance of adapters must be passed in when instantiating SM Account Use Cases library.' ) } @@ -28,6 +28,7 @@ class smAccountUseCases { console.log('Checking for new Social Media Accounts...') const messages = await this.adapters.nostr.readGlobalFeed() + console.log('messages: ', messages) for (let i = 0; i < messages.length; i++) { const msg = messages[i] // const { content, id, tags, npub, pubkey } = msg @@ -122,6 +123,7 @@ class smAccountUseCases { const followerCnt = followList.length await SmAccount.updateOne({ _id: account._id }, { $set: { followerCnt } }) } + return true } catch (err) { console.error('Error in smAccount-use-cases.js/updateSmAccounts(): ', err) throw err diff --git a/test/unit/controllers/rest-api/offer/offer.rest.controller.unit.js b/test/unit/controllers/rest-api/offer/offer.rest.controller.unit.js index dfd86a9..f470843 100644 --- a/test/unit/controllers/rest-api/offer/offer.rest.controller.unit.js +++ b/test/unit/controllers/rest-api/offer/offer.rest.controller.unit.js @@ -91,25 +91,143 @@ describe('#Offer-REST-Router', () => { // assert.equal(ctx.body.hash, 'testHash') }) - // it('should catch and throw an error', async () => { - // try { - // ctx.request.body = { - // offer: {} - // } - // - // // Force an error - // sandbox - // .stub(uut.useCases.offer, 'createOffer') - // .rejects(new Error('test error')) - // - // await uut.createOffer(ctx) - // - // assert.fail('Unexpected code path') - // } catch (err) { - // // console.log('err: ', err) - // assert.include(err.message, 'test error') - // } - // }) + it('should catch and throw an error', async () => { + try { + ctx.request.body = { + offer: {} + } + + // Force an error + sandbox + .stub(uut.useCases.offer, 'createOffer') + .rejects(new Error('test error')) + + await uut.createOffer(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'test error') + } + }) + }) + + describe('#listOffers', () => { + it('should list offers', async () => { + ctx.params = { page: 0 } + sandbox.stub(uut.useCases.offer, 'listOffers').resolves([]) + await uut.listOffers(ctx) + assert.isArray(ctx.body) + }) + it('should catch and throw an error', async () => { + try { + ctx.params = { } + sandbox + .stub(uut.useCases.offer, 'listOffers') + .throws(new Error('test error')) + + await uut.listOffers(ctx) + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#listNftOffers', () => { + it('should list nft offers', async () => { + ctx.params = { page: 0 } + sandbox.stub(uut.useCases.offer, 'listNftOffers').resolves([]) + await uut.listNftOffers(ctx) + assert.isArray(ctx.body) + }) + it('should catch and throw an error', async () => { + try { + ctx.params = { page: 0 } + sandbox + .stub(uut.useCases.offer, 'listNftOffers') + .throws(new Error('test error')) + + await uut.listNftOffers(ctx) + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#listFungibleOffers', () => { + it('should list fungible offers', async () => { + ctx.params = { page: 0 } + sandbox.stub(uut.useCases.offer, 'listFungibleOffers').resolves([]) + await uut.listFungibleOffers(ctx) + assert.isArray(ctx.body) + }) + it('should catch and throw an error', async () => { + try { + ctx.params = { page: 0 } + sandbox + .stub(uut.useCases.offer, 'listFungibleOffers') + .throws(new Error('test error')) + + await uut.listFungibleOffers(ctx) + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#takeOffer', () => { + it('should take an offer', async () => { + ctx.request.body = { + nostrEventId: 'testEventId' + } + + sandbox.stub(uut.useCases.offer, 'takeOffer').resolves({ eventId: 'testEventId', noteId: 'testNoteId' }) + await uut.takeOffer(ctx) + assert.isObject(ctx.body) + assert.property(ctx.body, 'eventId') + assert.property(ctx.body, 'noteId') + }) + it('should catch and throw an error', async () => { + try { + ctx.request.body = { + nostrEventId: 'testEventId' + } + + sandbox + .stub(uut.useCases.offer, 'takeOffer') + .throws(new Error('test error')) + + await uut.takeOffer(ctx) + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#listOffersByAddress', () => { + it('should list offers by address', async () => { + ctx.params = { addr: 'testAddress' } + sandbox.stub(uut.useCases.offer, 'listOffersByAddress').resolves([]) + await uut.listOffersByAddress(ctx) + assert.isArray(ctx.body) + }) + it('should catch and throw an error', async () => { + try { + ctx.params = { addr: 'testAddress' } + sandbox + .stub(uut.useCases.offer, 'listOffersByAddress') + .throws(new Error('test error')) + + await uut.listOffersByAddress(ctx) + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) }) describe('#handleError', () => { diff --git a/test/unit/controllers/rest-api/order/order.rest.controller.unit.js b/test/unit/controllers/rest-api/order/order.rest.controller.unit.js index 5ec50ab..a02b4e9 100644 --- a/test/unit/controllers/rest-api/order/order.rest.controller.unit.js +++ b/test/unit/controllers/rest-api/order/order.rest.controller.unit.js @@ -101,7 +101,47 @@ describe('#Order-REST-Router', () => { } }) }) + describe('#listOrders', () => { + it('should list orders', async () => { + ctx.params = { page: 0 } + sandbox.stub(uut.useCases.order, 'listOrders').resolves([]) + await uut.listOrders(ctx) + assert.isArray(ctx.body) + }) + it('should catch and throw an error', async () => { + try { + ctx.params = { page: 0 } + sandbox.stub(uut.useCases.order, 'listOrders').throws(new Error('test error')) + await uut.listOrders(ctx) + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + describe('#deleteOrder', () => { + it('should delete an order', async () => { + ctx.request.body = { + nostrEventId: 'testEventId' + } + sandbox.stub(uut.useCases.order, 'deleteOrder').resolves('testTxid') + await uut.deleteOrder(ctx) + assert.equal(ctx.body.txid, 'testTxid') + }) + it('should catch and throw an error', async () => { + try { + ctx.request.body = { + nostrEventId: 'testEventId' + } + sandbox.stub(uut.useCases.order, 'deleteOrder').throws(new Error('test error')) + await uut.deleteOrder(ctx) + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) describe('#handleError', () => { it('should still throw error if there is no message', () => { try { diff --git a/test/unit/controllers/rest-api/order/order.rest.router.unit.js b/test/unit/controllers/rest-api/order/order.rest.router.unit.js index 3abeede..d8377fa 100644 --- a/test/unit/controllers/rest-api/order/order.rest.router.unit.js +++ b/test/unit/controllers/rest-api/order/order.rest.router.unit.js @@ -74,4 +74,13 @@ describe('#Order-REST-Router', () => { } }) }) + describe('#createOrder', () => { + it('should route to controller', async () => { + sandbox.stub(uut.validators, 'ensureUser').resolves(true) + const spy = sandbox.stub(uut.orderRESTController, 'createOrder').resolves(true) + + await uut.createOrder() + assert.isTrue(spy.calledOnce) + }) + }) }) diff --git a/test/unit/controllers/rest-api/p2wdb/p2wdb.rest.controller.unit.js b/test/unit/controllers/rest-api/p2wdb/p2wdb.rest.controller.unit.js new file mode 100644 index 0000000..e1f4b22 --- /dev/null +++ b/test/unit/controllers/rest-api/p2wdb/p2wdb.rest.controller.unit.js @@ -0,0 +1,160 @@ +/* + Unit tests for the REST API handler for the /sm endpoints. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Local support libraries +import adapters from '../../../mocks/adapters/index.js' +import UseCasesMock from '../../../mocks/use-cases/index.js' +import P2WDBRESTController from '../../../../../src/controllers/rest-api/p2wdb/controller.js' + +import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js' + +let uut +let sandbox +let ctx + +describe('#P2WDB-REST-Router', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new P2WDBRESTController({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + ctx = mockContext() + ctx.params = { } + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new P2WDBRESTController() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating /p2wdb REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new P2WDBRESTController({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating /p2wdb REST Controller.' + ) + } + }) + }) + + describe('#routeWebhook', () => { + it('should route the webhook for a new counter offer', async () => { + ctx.request = { + body: { + data: { + dataType: 'counter-offer' + } + } + } + sandbox.stub(uut.useCases.offer, 'acceptCounterOffer').resolves() + + await uut.routeWebhook(ctx) + + assert.isTrue(true) + }) + it('should route the webhook for a new offer', async () => { + ctx.request = { + body: { + data: { + dataType: 'offer' + } + } + } + sandbox.stub(uut.useCases.offer, 'createOffer').resolves() + + await uut.routeWebhook(ctx) + + assert.isTrue(true) + }) + it('should route the webhook flag', async () => { + ctx.request = { + body: { + data: { + dataType: 'flag' + } + } + } + sandbox.stub(uut.useCases.offer, 'flagOffer').resolves() + + await uut.routeWebhook(ctx) + + assert.isTrue(true) + }) + it('should handle unknown data type', async () => { + ctx.request = { + body: { + data: { + dataType: 'unknown' + } + } + } + sandbox.stub(uut.useCases.offer, 'flagOffer').resolves() + + await uut.routeWebhook(ctx) + + assert.isTrue(true) + }) + + it('should catch and throw an error', async () => { + try { + ctx.request.body = {} + + await uut.routeWebhook(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'Cannot read properties of undefined') + } + }) + describe('#handleError', () => { + it('should still throw error if there is no message', () => { + try { + const err = { + status: 404 + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'Not Found') + } + }) + it('should catch error if message is provided', () => { + try { + const err = { + status: 422, + message: 'Unprocessable Entity' + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'Unprocessable Entity') + } + }) + }) + }) +}) diff --git a/test/unit/controllers/rest-api/p2wdb/p2wdb.rest.router.unit.js b/test/unit/controllers/rest-api/p2wdb/p2wdb.rest.router.unit.js new file mode 100644 index 0000000..1c5d9d3 --- /dev/null +++ b/test/unit/controllers/rest-api/p2wdb/p2wdb.rest.router.unit.js @@ -0,0 +1,77 @@ +/* + Unit tests for the REST API handler for the /p2wdb endpoints. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Local support libraries +import adapters from '../../../mocks/adapters/index.js' +import UseCasesMock from '../../../mocks/use-cases/index.js' +import P2WDBRouter from '../../../../../src/controllers/rest-api/p2wdb/index.js' + +let uut +let sandbox +// let ctx + +// const mockContext = require('../../../../unit/mocks/ctx-mock').context + +describe('#P2WDB-REST-Router', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new P2WDBRouter({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + // ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new P2WDBRouter() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating /p2wdb REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new P2WDBRouter({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating /p2wdb REST Controller.' + ) + } + }) + }) + + describe('#attach', () => { + it('should throw an error if app is not passed in.', () => { + try { + uut.attach() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Must pass app object when attached REST API controllers.' + ) + } + }) + }) +}) diff --git a/test/unit/controllers/rest-api/smAccount/smAccount.rest.controller.unit.js b/test/unit/controllers/rest-api/smAccount/smAccount.rest.controller.unit.js new file mode 100644 index 0000000..a5b6b6a --- /dev/null +++ b/test/unit/controllers/rest-api/smAccount/smAccount.rest.controller.unit.js @@ -0,0 +1,203 @@ +/* + Unit tests for the REST API handler for the /sm endpoints. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Local support libraries +import adapters from '../../../mocks/adapters/index.js' +import UseCasesMock from '../../../mocks/use-cases/index.js' +import SmAccountRESTController from '../../../../../src/controllers/rest-api/smAccount/controller.js' + +import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js' + +let uut +let sandbox +let ctx + +describe('#SmAccount-REST-Router', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new SmAccountRESTController({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + ctx = mockContext() + ctx.params = { } + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new SmAccountRESTController() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating /sm REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new SmAccountRESTController({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating /sm REST Controller.' + ) + } + }) + }) + + describe('#listAccounts', () => { + it('should list all social media accounts', async () => { + ctx.params.page = 0 + + // Mock dependencies + sandbox.stub(uut.useCases.smAccount, 'listAccounts').resolves([]) + + await uut.listAccounts(ctx) + + assert.isArray(ctx.body) + }) + + it('should catch and throw an error', async () => { + try { + ctx.params.page = 0 + + // Force an error + sandbox.stub(uut.useCases.smAccount, 'listAccounts') + .throws(new Error('test error')) + + await uut.listAccounts(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err: ', err) + assert.include(err.message, 'test error') + } + }) + }) + + describe('#getAccountByNpub', () => { + it('should get a social media account by npub', async () => { + sandbox.stub(uut.useCases.smAccount, 'getAccountByNpub').resolves({ npub: 'testNpub' }) + ctx.params.npub = 'testNpub' + + await uut.getAccountByNpub(ctx) + }) + it('should throw 400 if npub is not provided', async () => { + try { + await uut.getAccountByNpub(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'Npub is required') + } + }) + it('should throw 400 if npub is not found', async () => { + try { + sandbox.stub(uut.useCases.smAccount, 'getAccountByNpub').resolves(null) + ctx.params.npub = 'testNpub' + await uut.getAccountByNpub(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'Account not found') + } + }) + it('should catch and throw an error', async () => { + try { + sandbox.stub(uut.useCases.smAccount, 'getAccountByNpub').throws(new Error('test error')) + ctx.params.npub = 'testNpub' + + await uut.getAccountByNpub(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#getAccountByBchAddr', () => { + it('should get a social media account by npub', async () => { + sandbox.stub(uut.useCases.smAccount, 'getAccountByBchAddr').resolves({ bchAddr: 'testBchAddr' }) + ctx.params.bchAddr = 'testBchAddr' + + await uut.getAccountByBchAddr(ctx) + }) + + it('should throw 400 if bchAddr is not provided', async () => { + try { + await uut.getAccountByBchAddr(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'BCH address is required') + } + }) + + it('should throw 400 if bchAddr is not found', async () => { + try { + sandbox.stub(uut.useCases.smAccount, 'getAccountByBchAddr').resolves(null) + ctx.params.bchAddr = 'testBchAddr' + await uut.getAccountByBchAddr(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'Account not found') + } + }) + it('should catch and throw an error', async () => { + try { + sandbox.stub(uut.useCases.smAccount, 'getAccountByBchAddr').throws(new Error('test error')) + ctx.params.bchAddr = 'testBchAddr' + + await uut.getAccountByBchAddr(ctx) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#handleError', () => { + it('should still throw error if there is no message', () => { + try { + const err = { + status: 404 + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'Not Found') + } + }) + it('should catch error if message is provided', () => { + try { + const err = { + status: 422, + message: 'Unprocessable Entity' + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'Unprocessable Entity') + } + }) + }) +}) diff --git a/test/unit/controllers/rest-api/smAccount/smAccount.rest.router.unit.js b/test/unit/controllers/rest-api/smAccount/smAccount.rest.router.unit.js new file mode 100644 index 0000000..91a0a53 --- /dev/null +++ b/test/unit/controllers/rest-api/smAccount/smAccount.rest.router.unit.js @@ -0,0 +1,77 @@ +/* + Unit tests for the REST API handler for the /sm endpoints. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Local support libraries +import adapters from '../../../mocks/adapters/index.js' +import UseCasesMock from '../../../mocks/use-cases/index.js' +import SmAccountRouter from '../../../../../src/controllers/rest-api/smAccount/index.js' + +let uut +let sandbox +// let ctx + +// const mockContext = require('../../../../unit/mocks/ctx-mock').context + +describe('#SmAccount-REST-Router', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new SmAccountRouter({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + // ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new SmAccountRouter() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating /sm REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new SmAccountRouter({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating /sm REST Controller.' + ) + } + }) + }) + + describe('#attach', () => { + it('should throw an error if app is not passed in.', () => { + try { + uut.attach() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Must pass app object when attached REST API controllers.' + ) + } + }) + }) +}) diff --git a/test/unit/controllers/rest-api/users/users.rest.router.unit.js b/test/unit/controllers/rest-api/users/users.rest.router.unit.js index 3352a60..70479f7 100644 --- a/test/unit/controllers/rest-api/users/users.rest.router.unit.js +++ b/test/unit/controllers/rest-api/users/users.rest.router.unit.js @@ -108,25 +108,25 @@ describe('#Users-REST-Router', () => { }) }) - // describe('#getAll', () => { - // it('should route to controller', async () => { - // sandbox.stub(uut.validators, 'ensureUser').resolves(true) - // const spy = sandbox.stub(uut.userRESTController, 'getUsers').resolves(true) + describe('#getAll', () => { + it('should route to controller', async () => { + sandbox.stub(uut.validators, 'ensureAdmin').resolves(true) + const spy = sandbox.stub(uut.userRESTController, 'getUsers').resolves(true) - // await uut.getAll() - // assert.isTrue(spy.calledOnce) - // }) - // }) + await uut.getAll() + assert.isTrue(spy.calledOnce) + }) + }) - // describe('#getById', () => { - // it('should route to controller', async () => { - // sandbox.stub(uut.validators, 'ensureUser').resolves(true) - // const spy = sandbox.stub(uut.userRESTController, 'getUser').resolves(true) + describe('#getById', () => { + it('should route to controller', async () => { + sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true) + const spy = sandbox.stub(uut.userRESTController, 'getUser').resolves(true) - // await uut.getById() - // assert.isTrue(spy.calledOnce) - // }) - // }) + await uut.getById() + assert.isTrue(spy.calledOnce) + }) + }) describe('#updateUser', () => { it('should route to controller', async () => { diff --git a/test/unit/controllers/timer-controllers.unit.js b/test/unit/controllers/timer-controllers.unit.js index 6a4b01c..ca46d99 100644 --- a/test/unit/controllers/timer-controllers.unit.js +++ b/test/unit/controllers/timer-controllers.unit.js @@ -130,12 +130,12 @@ describe('#Timer-Controllers', () => { assert.equal(result, true) }) - // it('should return false on error', async () => { - // sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error')) - // const result = await uut.cleanUsage() + it('should return false on error', async () => { + sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error')) + const result = await uut.cleanUsage() - // assert.equal(result, false) - // }) + assert.equal(result, false) + }) }) describe('#backupUsage', () => { @@ -154,4 +154,37 @@ describe('#Timer-Controllers', () => { assert.equal(result, false) }) }) + + describe('#newSmAccounts', () => { + it('should kick off the Use Case', async () => { + const result = await uut.newSmAccounts() + + assert.equal(result, true) + }) + + it('should return false on error', async () => { + sandbox.stub(uut.useCases.smAccount, 'checkForNewSmAccounts').throws(new Error('test error')) + // sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error')) + + const result = await uut.newSmAccounts() + + assert.equal(result, false) + }) + }) + describe('#updateSmAccounts', () => { + it('should kick off the Use Case', async () => { + const result = await uut.updateSmAccounts() + + assert.equal(result, true) + }) + + it('should return false on error', async () => { + sandbox.stub(uut.useCases.smAccount, 'updateSmAccounts').throws(new Error('test error')) + // sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error')) + + const result = await uut.updateSmAccounts() + + assert.equal(result, false) + }) + }) }) diff --git a/test/unit/entities/offer.entity.unit.js b/test/unit/entities/offer.entity.unit.js index dea0243..2d3c913 100644 --- a/test/unit/entities/offer.entity.unit.js +++ b/test/unit/entities/offer.entity.unit.js @@ -237,6 +237,113 @@ describe('#Offer-Entity', () => { } }) + it('should throw an error if makerAddr is not included', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + operatorPercentage: 10 + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'makerAddr' must be a string.") + } + }) + it('should throw an error if makerAddr is not a string', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + makerAddr: 1234567890 + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'makerAddr' must be a string.") + } + }) + + it('should throw an error if tokenType is not included', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + operatorPercentage: 10 + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'tokenType' must be a number.") + } + }) + + it('should throw an error if tokenType is not a number', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + tokenType: 'notANumber' + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'tokenType' must be a number.") + } + }) + it('should throw an error if nostrEventId is not included', () => { try { const offerData = { @@ -267,6 +374,124 @@ describe('#Offer-Entity', () => { } }) + it('should throw an error if operatorAddress is not included', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + tokenType: 1, + nostrEventId: 'test', + operatorPercentage: 10 + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'operatorAddress' must be a string.") + } + }) + + it('should throw an error if operatorAddress is not a string', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + tokenType: 1, + nostrEventId: 'test', + operatorAddress: 1234567890, + operatorPercentage: 10 + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'operatorAddress' must be a string.") + } + }) + + it('should throw an error if operatorPercentage is not included', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + tokenType: 1, + nostrEventId: 'test', + operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00' + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'operatorPercentage' must be a number.") + } + }) + + it('should throw an error if operatorPercentage is not a number', () => { + try { + const offerData = { + data: { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1, + utxoTxid: 'fakeTxid', + utxoVout: 0, + offerStatus: 'posted', + makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + tokenType: 1, + nostrEventId: 'test', + operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00', + operatorPercentage: 'notANumber' + } + } + uut.validate(offerData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'operatorPercentage' must be a number.") + } + }) + it('should validate a new offer', () => { const offerObj = { appId: 'swapTest555', diff --git a/test/unit/entities/order.entity.unit.js b/test/unit/entities/order.entity.unit.js index 9a80d4b..3e9afb1 100644 --- a/test/unit/entities/order.entity.unit.js +++ b/test/unit/entities/order.entity.unit.js @@ -130,5 +130,71 @@ describe('#Order-Entity', () => { assert.include(err.message, "Property 'numTokens' must be a number.") } }) + it('should throw an error if numTokens is not a number', () => { + try { + const data = { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 'notANumber' + } + uut.inputValidate(data) + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'numTokens' must be a number.") + } + }) + it('should throw an error if makerAddress is not included', () => { + try { + const data = { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1000 + } + uut.inputValidate(data) + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'makerAddr' must be a string.") + } + }) + it('should throw an error if makerAddress is not a string', () => { + try { + const data = { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1000, + makerAddr: 1234567890 + } + uut.inputValidate(data) + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'makerAddr' must be a string.") + } + }) + it('should return the correct data', () => { + const data = { + messageType: 1, + messageClass: 1, + tokenId: 'fakeId', + buyOrSell: 'buy', + rateInBaseUnit: 1000, + minUnitsToExchange: 350, + numTokens: 1000, + makerAddr: 'fakeAddr' + } + const result = uut.inputValidate(data) + assert.deepEqual(result, data) + }) }) }) diff --git a/test/unit/mocks/adapters/index.js b/test/unit/mocks/adapters/index.js index 57a6b5a..a8da981 100644 --- a/test/unit/mocks/adapters/index.js +++ b/test/unit/mocks/adapters/index.js @@ -150,6 +150,18 @@ const localdb = { static find () {} static findOne () {} + async save () { + return {} + } + }, + SmAccount: class SmAccount { + constructor (obj) {} + + static findById () {} + static find () {} + static findOne () {} + static updateOne () {} + async save () { return {} } @@ -202,7 +214,9 @@ const p2wdb = { const nostr = { post: async () => {return true }, read: async () => { return true }, - eventId2note: () => { return 'testNoteId' } + eventId2note: () => { return 'testNoteId' }, + readGlobalFeed: async () => { return [] }, + getFollowers: async () => { return [] } } export default { ipfs, localdb, bch, wallet, p2wdb, bchjs, nostr} diff --git a/test/unit/mocks/social-media-mocks.js b/test/unit/mocks/social-media-mocks.js new file mode 100644 index 0000000..f40b4ec --- /dev/null +++ b/test/unit/mocks/social-media-mocks.js @@ -0,0 +1,42 @@ + + +const messagesMock = [ + { + content: JSON.stringify("I'm testing so hard!!!"), + id: '1', + tags: [['u', 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477']], + npub: 'npub1234567890', + pubkey: '1234567890', + } +] +const smAccountsMock = [ + { + npub: 'npub1234567890', + bchAddr: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477', + pubkey: '1234567890', + pfp: '', + createdAt: new Date(), + updatedAt: new Date(), + followerCnt: 0, + followingCnt: 0, + posts: 0, + likes: 0, + comments: 0, + reposts: 0 + }, + { + npub: 'npub1234567891', + bchAddr: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478', + pubkey: '1234567891', + pfp: '', + createdAt: new Date(), + updatedAt: new Date(), + followerCnt: 0, + followingCnt: 0, + posts: 0, + likes: 0, + comments: 0, + reposts: 0 + } +] +export default { messagesMock, smAccountsMock } \ No newline at end of file diff --git a/test/unit/mocks/use-cases/index.js b/test/unit/mocks/use-cases/index.js index 522d8fb..fac7dd6 100644 --- a/test/unit/mocks/use-cases/index.js +++ b/test/unit/mocks/use-cases/index.js @@ -50,6 +50,27 @@ class Offer { async loadOffers(){ } + async listOffers() { + return [] + } + async listNftOffers() { + return [] + } + async listFungibleOffers() { + return [] + } + async takeOffer() { + return [] + } + async listOffersByAddress() { + return [] + } + async flagOffer() { + return {} + } + async acceptCounterOffer() { + return {} + } } class Order { @@ -59,6 +80,12 @@ class Order { async removeStaleOrders(){ return {} } + async listOrders() { + return [] + } + async deleteOrder() { + return 'testTxid' + } } class UsageUseCaseMock { @@ -87,6 +114,24 @@ class UsageUseCaseMock { } } +class SmAccountUseCaseMock { + async listAccounts() { + return [] + } + async getAccountByNpub() { + return {} + } + async getAccountByBchAddr() { + return {} + } + async checkForNewSmAccounts() { + return true + } + async updateSmAccounts() { + return true + } +} + class UseCasesMock { constuctor(localConfig = {}) { // this.user = new UserUseCaseMock(localConfig) @@ -97,6 +142,7 @@ class UseCasesMock { offer = new Offer() order = new Order() usage = new UsageUseCaseMock() + smAccount = new SmAccountUseCaseMock() } export default UseCasesMock; diff --git a/test/unit/mocks/use-cases/offer-mock-data.js b/test/unit/mocks/use-cases/offer-mock-data.js index 071e82e..a814667 100644 --- a/test/unit/mocks/use-cases/offer-mock-data.js +++ b/test/unit/mocks/use-cases/offer-mock-data.js @@ -169,6 +169,7 @@ const deserealizeTxMockNoOperatorOut = { addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478'] } }, + //... ] } diff --git a/test/unit/use-cases/offer.use-case.unit.js b/test/unit/use-cases/offer.use-case.unit.js index a047efc..35c67be 100644 --- a/test/unit/use-cases/offer.use-case.unit.js +++ b/test/unit/use-cases/offer.use-case.unit.js @@ -5,6 +5,7 @@ // Public npm libraries import { assert } from 'chai' import sinon from 'sinon' +import RetryQueue from '@chris.troutner/retry-queue' // Local support libraries // const testUtils = require('../../utils/test-utils') @@ -30,6 +31,7 @@ describe('#offer-use-case', () => { const order = new OrderUseCase({ adapters }) uut = new OfferLib({ adapters, order }) + uut.retryQueue = new RetryQueue({ retryPeriod: 1000, attempts: 1 }) }) afterEach(() => sandbox.restore()) @@ -85,6 +87,15 @@ describe('#offer-use-case', () => { assert.isFalse(result) }) + it('should return false if offer already processed', async () => { + const offerObj = mockData.offerMockData + uut.seenOffers.push(offerObj.data.nostrEventId) + // Mock dependencies + + const result = await uut.createOffer(offerObj) + assert.isFalse(result) + }) + it('should return false for invalid utxo', async () => { const offerObj = mockData.offerMockData @@ -334,6 +345,14 @@ describe('#offer-use-case', () => { const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' }) assert.isFalse(result) }) + it('should fetch alternative url', async () => { + // Mock dependencies and force desired code path. + sandbox.stub(uut.axios, 'get').onCall(0).throws(new Error()) + .onCall(1).resolves({ data: { nsfw: true } }) + + const result = await uut.detectNsfw({ mutableData: 'ipfs://bafybeibqnsmmh6bkf2wwextetki4tly65z4r4qkrrpl5xwgvzdzjley6wm' }) + assert.isTrue(result) + }) }) describe('#listOffers', () => { @@ -597,7 +616,7 @@ describe('#offer-use-case', () => { }) }) - describe('#findOrderByEvent', () => { + describe('#findOfferByEvent', () => { it('should throw an error if hash is not provided', async () => { try { await uut.findOfferByEvent() @@ -629,6 +648,29 @@ describe('#offer-use-case', () => { }) }) + describe('#listOffersByAddress', () => { + it('should throw an error if order is not found!', async () => { + try { + // Mock dependencies + sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error')) + + await uut.listOffersByAddress('eventId') + + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + + it('should return offer by eventId', async () => { + // Mock dependencies + sandbox.stub(uut.OfferModel, 'find').resolves([]) + + const result = await uut.listOffersByAddress() + assert.isArray(result) + }) + }) + describe('#flagOffer', () => { it('should throw an error if input is not provided', async () => { try { @@ -687,23 +729,84 @@ describe('#offer-use-case', () => { it('should skip internal function errors ', async () => { // Mock dependencies - sandbox.stub(uut.adapters.nostr, 'read').resolves([mockData.offerMockData]) + const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }] + sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock) await uut.loadOffers() }) - - it('should review and load offers', async () => { + it('should createOffer ', async () => { + mockData.offerMockData.data.dataType = 'offer' // Mock dependencies - sandbox.stub(uut.adapters.nostr, 'read').resolves([JSON.stringify(mockData.offerMockData)]) + const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }] + sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock) + const spy = sandbox.stub(uut, 'createOffer').resolves(true) await uut.loadOffers() + assert.isTrue(spy.calledOnce) + }) + it('should accept counter Offer ', async () => { + mockData.offerMockData.data.dataType = 'counter-offer' + // Mock dependencies + const nostrReadMock = [{ content: JSON.stringify(mockData.offerMockData) }] + sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock) + const spy = sandbox.stub(uut, 'acceptCounterOffer').resolves(true) + + await uut.loadOffers() + assert.isTrue(spy.calledOnce) + }) + + it('should skip error inside loop', async () => { + // Mock dependencies + const nostrReadMock = [{ content: '' }] + sandbox.stub(uut.adapters.nostr, 'read').resolves(nostrReadMock) + await uut.loadOffers() }) }) describe('#acceptCounterOffer', () => { + it('should return false if order already processed', async () => { + const offerObj = mockData.offerMockData + uut.seenOffers.push(offerObj.data.nostrEventId) + // Mock dependencies + + const result = await uut.acceptCounterOffer(offerObj) + assert.isFalse(result) + }) + it('should return if order is not found!', async () => { // Mock dependencies - sandbox.stub(uut.orderUseCase, 'findOrderByEvent').throws(new Error('test error')) + sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').throws(new Error('test error')) + + const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) + assert.equal(result, 'N/A') + }) + + it('should return if utxo cant be validated', async () => { + // Mock dependencies + const mock = Object.assign({}, mockData.offerMockData.data) + sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) + sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(new Error('test error')) + + const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) + assert.equal(result, 'N/A') + }) + + it('should handle axios error', async () => { + // Mock dependencies + const mock = Object.assign({}, mockData.offerMockData.data) + const stubErr = new Error() + stubErr.isAxiosError = true + sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) + sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').throws(stubErr) + + const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) + assert.equal(result, 'N/A') + }) + it('should return if utxo is invalid', async () => { + // Mock dependencies + const mock = Object.assign({}, mockData.offerMockData.data) + sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) + sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').resolves(false) const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) assert.equal(result, 'N/A') @@ -719,7 +822,7 @@ describe('#offer-use-case', () => { sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) - const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) + const result = await uut.acceptCounterOffer({ data: {} }) console.log('result: ', result) assert.fail('unexpected code path') } catch (err) { @@ -734,7 +837,6 @@ describe('#offer-use-case', () => { // Mock dependencies sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) - sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mockData.offerMockData.data) sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) @@ -754,7 +856,6 @@ describe('#offer-use-case', () => { mock.numTokens = 0 // Mock dependencies - sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0) @@ -763,6 +864,27 @@ describe('#offer-use-case', () => { const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) assert.equal(result, 'N/A') }) + it('should skip transactions if operator address does not match', async () => { + try { + // Mock data + const mock = Object.assign({}, mockData.offerMockData.data) + mock.makerAddr = mockData.deserealizeTxMockNoOperatorOut.vout[2].scriptPubKey.addresses[0] + mock.operatorAddress = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' + mock.rateInBaseUnit = 0 + mock.numTokens = 0 + + // Mock dependencies + sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) + sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0) + + sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) + + await uut.acceptCounterOffer({ data: {} }) + assert.fail('unexpected code path') + } catch (error) { + assert.include(error.message, 'The Counter Offer has an output address of') + } + }) it('should handle error for wrong transaction output address', async () => { try { @@ -779,7 +901,7 @@ describe('#offer-use-case', () => { sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) - await uut.acceptCounterOffer({ data: { /** .... */ } }) + await uut.acceptCounterOffer({ data: {} }) assert.fail('unexpected code path') } catch (err) { assert.include(err.message, 'The Counter Offer has an output address of') @@ -787,21 +909,44 @@ describe('#offer-use-case', () => { } }) - it('should return tx id', async () => { + it('should handle error if operator sats to receive cant be calculated', async () => { // Mock data const mock = Object.assign({}, mockData.offerMockData.data) - mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] // Maker Address + mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] + mock.operatorAddress = mockData.deserealizeTxMock.vout[3].scriptPubKey.addresses[0] mock.rateInBaseUnit = 0 mock.numTokens = 0 // Mock dependencies sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) + sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0) + + sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) + + const result = await uut.acceptCounterOffer({ data: {} }) + assert.equal(result, 'N/A') + }) + + it('should return tx id', async () => { + // Mock data + const mock = Object.assign({}, mockData.offerMockData.data) + mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] + mock.operatorAddress = mockData.deserealizeTxMock.vout[3].scriptPubKey.addresses[0] + mock.rateInBaseUnit = 0 + mock.numTokens = 0 + + // Mock dependencies + sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) + sandbox.stub(uut.orderUseCase, 'findOrderByUtxo').resolves(mock) + sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').onCall(0).returns(0).onCall(1).returns(1000) + sandbox.stub(uut.adapters.localdb.Users, 'findById').resolves({ mnemonic: 'test' }) sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMock) // - const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) + const result = await uut.acceptCounterOffer({ data: {} }) assert.isString(result) + assert.notEqual('N/A') }) }) }) diff --git a/test/unit/use-cases/order.use-case.unit.js b/test/unit/use-cases/order.use-case.unit.js index 0fe3bab..a30abcf 100644 --- a/test/unit/use-cases/order.use-case.unit.js +++ b/test/unit/use-cases/order.use-case.unit.js @@ -158,6 +158,7 @@ describe('#order-use-case', () => { describe('#createOrder', () => { it('should create an order and return the hash', async () => { + uut.config.useFullStackCash = true const entryObj = { lokadId: 'SWP', messageType: 1, @@ -386,4 +387,55 @@ describe('#order-use-case', () => { assert.isString(result) }) }) + + describe('#findOrderByUtxo', () => { + it('should throw an error if data is not provided', async () => { + try { + await uut.findOrderByUtxo({}) + } catch (err) { + assert.include(err.message, 'Cannot read properties of undefined') + } + }) + it('should throw an error if order is not found', async () => { + try { + sandbox.stub(uut.OrderModel, 'findOne').resolves(null) + const offerData = { + data: { + utxoTxid: 'utxoTxid', + utxoVout: 'utxoVout' + } + } + await uut.findOrderByUtxo(offerData) + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'order not found') + } + }) + it('should throw an error if order event id is not found', async () => { + try { + sandbox.stub(uut.OrderModel, 'findOne').resolves({ nostrEventId: null }) + const offerData = { + data: { + utxoTxid: 'utxoTxid', + utxoVout: 'utxoVout' + } + } + await uut.findOrderByUtxo(offerData) + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'order not found') + } + }) + it('should return order by utxo', async () => { + sandbox.stub(uut.OrderModel, 'findOne').resolves({ toObject: () => { return {} }, nostrEventId: 'nostrEventId' }) + const offerData = { + data: { + utxoTxid: 'utxoTxid', + utxoVout: 'utxoVout' + } + } + const result = await uut.findOrderByUtxo(offerData) + assert.isObject(result) + }) + }) }) diff --git a/test/unit/use-cases/sm-account.unit.js b/test/unit/use-cases/sm-account.unit.js new file mode 100644 index 0000000..f0732e9 --- /dev/null +++ b/test/unit/use-cases/sm-account.unit.js @@ -0,0 +1,166 @@ +/* + Unit tests for the SM Account Use Case library. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Unit under test (uut) +import SMAccountLib from '../../../src/use-cases/smAccount-use-cases.js' +import SMMock from '../mocks/social-media-mocks.js' +// Local support libraries +import adapters from '../mocks/adapters/index.js' + +describe('#sm-account-use-case', () => { + let uut + let sandbox + + before(async () => { + // Delete all previous users in the database. + // await testUtils.deleteAllUsers() + }) + + beforeEach(() => { + sandbox = sinon.createSandbox() + + uut = new SMAccountLib({ adapters }) + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new SMAccountLib() + + assert.fail('Unexpected code path') + console.log(uut) // linter + } catch (err) { + assert.include( + err.message, + 'Instance of adapters must be passed in when instantiating SM Account Use Cases library.' + ) + } + }) + }) + + describe('#checkForNewSmAccounts', () => { + it('check for new SM accounts', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').resolves(SMMock.messagesMock) + + const result = await uut.checkForNewSmAccounts() + assert.isTrue(result) + }) + it('should skip existing SM accounts', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves({ npub: SMMock.messagesMock[0].npub }) + sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').resolves(SMMock.messagesMock) + + const result = await uut.checkForNewSmAccounts() + assert.isTrue(result) + }) + it('should throw an error if the readGlobalFeed fails', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.nostr, 'readGlobalFeed').rejects(new Error('Failed to read global feed')) + + try { + await uut.checkForNewSmAccounts() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'Failed to read global feed') + } + }) + }) + describe('#listAccounts', () => { + it('should return a list of SM accounts', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'find').callsFake(() => { + return { + skip: () => { + return { + limit: () => { + return SMMock.messagesMock + } + } + } + } + }) + + const result = await uut.listAccounts() + assert.isArray(result) + }) + it('should throw an error if the find fails', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'find').throws(new Error('Failed to find SM accounts')) + + try { + await uut.listAccounts() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'Failed to find SM accounts') + } + }) + }) + describe('#getAccountByNpub', () => { + it('should return a SM account by npub', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves(SMMock.messagesMock[0]) + + const result = await uut.getAccountByNpub(SMMock.messagesMock[0].npub) + assert.isObject(result) + }) + it('should catch error', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').throws(new Error('Failed to find SM account by npub')) + + try { + await uut.getAccountByNpub(SMMock.messagesMock[0].npub) + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'Failed to find SM account by npub') + } + }) + }) + describe('#getAccountByBchAddr', () => { + it('should return a SM account by bch addr', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').resolves(SMMock.messagesMock[0]) + + const result = await uut.getAccountByBchAddr('bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477') + assert.isObject(result) + }) + it('should catch error', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'findOne').throws(new Error('Failed to find SM account by bch addr')) + + try { + await uut.getAccountByBchAddr('bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477') + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'Failed to find SM account by bch addr') + } + }) + }) + describe('#updateSmAccounts', () => { + it('should update the SM accounts', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'find').resolves(SMMock.smAccountsMock) + + const result = await uut.updateSmAccounts() + assert.isTrue(result) + }) + it('should catch error', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.localdb.SmAccount, 'find').throws(new Error('Failed to find SM accounts')) + + try { + await uut.updateSmAccounts() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'Failed to find SM accounts') + } + }) + }) +})