From 9dbd18d0d6b6e0ed5c4ced878d47e5fec740047b Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Wed, 4 Dec 2024 06:51:28 -0400 Subject: [PATCH] Increased test coverage --- src/controllers/rest-api/offer/controller.js | 6 +- src/use-cases/offer/index.js | 123 ++--- test/unit/mocks/adapters/index.js | 6 +- test/unit/mocks/adapters/wallet.js | 1 + test/unit/mocks/use-cases/offer-mock-data.js | 102 ++-- test/unit/use-cases/offer.use-case.unit.js | 484 +++++++++++++++++++ 6 files changed, 625 insertions(+), 97 deletions(-) diff --git a/src/controllers/rest-api/offer/controller.js b/src/controllers/rest-api/offer/controller.js index 968e3bc..7f89ea8 100644 --- a/src/controllers/rest-api/offer/controller.js +++ b/src/controllers/rest-api/offer/controller.js @@ -100,11 +100,11 @@ class OfferRESTControllerLib { try { console.log('REST API controller, body: ', ctx.request.body) - const offerCid = ctx.request.body.offerCid + const nostrEventId = ctx.request.body.nostrEventId - const hash = await _this.useCases.offer.takeOffer(offerCid) + const eventId = await _this.useCases.offer.takeOffer(nostrEventId) - ctx.body = { hash } + ctx.body = { eventId } } catch (err) { wlogger.error('Error in takeOffer() REST API handler.') _this.handleError(ctx, err) diff --git a/src/use-cases/offer/index.js b/src/use-cases/offer/index.js index d73ed0d..cfb8e9b 100644 --- a/src/use-cases/offer/index.js +++ b/src/use-cases/offer/index.js @@ -273,12 +273,12 @@ class OfferUseCases { // provide functionality to take less than the total amount of tokens offered // (offerInfo.numTokens). Taking less than the offered amount will be added // in the future. - async takeOffer (offerCid) { + async takeOffer (eventId) { try { - console.log('offerCid: ', offerCid) + if (!eventId || typeof eventId !== 'string') throw new Error('eventId must be a string') // Get the Offer information - const offerInfo = await this.findOfferByHash(offerCid) + const offerInfo = await this.findOfferByEvent(eventId) console.log(`offerInfo: ${JSON.stringify(offerInfo, null, 2)}`) // Ensure the offer is in a 'posted' state and not already 'taken' @@ -291,6 +291,8 @@ class OfferUseCases { tx_hash: offerInfo.utxoTxid, tx_pos: offerInfo.utxoVout } + + // Note : should be added to retry-queue? const utxoStatus = await this.adapters.wallet.bchWallet.utxoIsValid(utxo) console.log('utxoStatus: ', utxoStatus) if (!utxoStatus) { @@ -310,6 +312,7 @@ class OfferUseCases { // Calculate amount of sats to generate a counter offer. let satsToMove = Math.ceil(offerInfo.numTokens * parseInt(offerInfo.rateInBaseUnit)) + console.log('satsToMove', satsToMove, offerInfo) if (isNaN(satsToMove)) { throw new Error('Could not calculate the amount of BCH to generate counter offer') } @@ -335,9 +338,9 @@ class OfferUseCases { // Create valid Offer object const takenOfferInfo = Object.assign({}, offerInfo) takenOfferInfo.partialTxHex = partialTxHex - delete takenOfferInfo.p2wdbHash + delete takenOfferInfo.nostrEventId delete takenOfferInfo._id - takenOfferInfo.offerHash = offerInfo.p2wdbHash + takenOfferInfo.offerHash = offerInfo.nostrEventId // Add P2WDB specific flag for signaling that this is a new offer. takenOfferInfo.dataType = 'counter-offer' @@ -345,19 +348,19 @@ class OfferUseCases { // Write offer info to the P2WDB // TODO: This will trigger the webhook. Find some way of triggering the // webhook on new offers, but not on counteroffers - const p2wdbObj = { + const nostrData = { wif: this.adapters.wallet.bchWallet.walletInfo.privateKey, data: takenOfferInfo, appId: this.config.p2wdbAppId } - const hash = await this.adapters.p2wdb.write(p2wdbObj) + const resultEventId = await this.adapters.nostr.post(nostrData) // Delete the Offer from the database, so that the user doesn't attempt // to take the offer more than once. offerInfo.remove() // Return the P2WDB CID - return hash + return resultEventId // return 'fake-hash' } catch (err) { @@ -380,6 +383,7 @@ class OfferUseCases { await this.adapters.wallet.bchWallet.initialize() // Ensure the app wallet has enough funds to write to the P2WDB. + // Note : this validation should be deprecated for nostr functionality? const wif = this.adapters.wallet.bchWallet.walletInfo.privateKey const canWriteToP2WDB = await this.adapters.p2wdb.checkForSufficientFunds(wif) if (!canWriteToP2WDB) throw new Error('App wallet does not have funds for writing to the P2WDB.') @@ -420,28 +424,27 @@ class OfferUseCases { } } - async findOfferByHash (p2wdbHash) { - // try { - if (typeof p2wdbHash !== 'string' || !p2wdbHash) { - throw new Error('p2wdbHash must be a string') + // Retrieve an Order model from the database. Find it by its event Id. + async findOfferByEvent (nostrEventId) { + try { + if (typeof nostrEventId !== 'string' || !nostrEventId) { + throw new Error('nostrEventId must be a string') + } + + const order = await this.OfferModel.findOne({ nostrEventId }) + + if (!order) { + throw new Error('offer not found') + } + + const orderObject = order.toObject() + // return this.offerEntity.validateFromModel(offerObject) + + return orderObject + } catch (err) { + console.error('Error in findOfferByEvent()') + throw err } - - const offer = await this.OfferModel.findOne({ p2wdbHash }) - - if (!offer) { - throw new Error('offer not found') - } - - return offer - - // const offerObject = offer.toObject() - // return this.offerEntity.validateFromModel(offerObject) - - // return offerObject - // } catch (err) { - // // console.error('Error in findOffer(): ', err) - // throw errByHash - // } } async findOfferByTxid (utxoTxid) { @@ -468,13 +471,15 @@ class OfferUseCases { // Counter Offer is passed to bch-dex by the P2WDB, the data is then passed // to this function. It does due dilligence on the Counter Offer, then signs // and broadcasts the transaction to accept the Counter Offer. - async acceptCounterOffer (p2wdbData) { + async acceptCounterOffer (offerData) { try { - console.log(`acceptCounterOffer() p2wdbData: ${JSON.stringify(p2wdbData, null, 2)}`) + console.log(`acceptCounterOffer() offerData: ${JSON.stringify(offerData, null, 2)}`) // See if this instance of bch-dex is managing the Order associated with // the incoming Counter Offer. - const orderHash = p2wdbData.data.offerHash + + // Note : this should be handle by nostrEvent id or UtxoId? + const orderHash = offerData.data.nostrEventId let orderData = {} try { orderData = await this.orderUseCase.findOrderByEvent(orderHash) @@ -485,12 +490,13 @@ class OfferUseCases { } // Deserialize the partially signed transaction. - const txHex = p2wdbData.data.partialTxHex + const txHex = offerData.data.partialTxHex const txObj = await this.adapters.wallet.deseralizeTx(txHex) console.log(`txObj: ${JSON.stringify(txObj, null, 2)}`) // Ensure the 3rd output (vout=2) contains the required amount of BCH. const satsToReceive = Math.ceil(orderData.numTokens * parseInt(orderData.rateInBaseUnit)) + console.log('Ceil', satsToReceive) if (isNaN(satsToReceive)) { throw new Error('Could not calculate the amount of BCH offered in the Counter Offer') } @@ -623,32 +629,39 @@ class OfferUseCases { } } - async flagOffer (flagData) { - console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`) + async flagOffer (flagData = {}) { + try { + if (!flagData.data) throw new Error('"data" property is required') - const p2wdbHash = flagData.data.p2wdbHash + console.log(`flagData: ${JSON.stringify(flagData, null, 2)}`) - // Get the offer from the database. - const offer = await this.findOfferByHash(p2wdbHash) - console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`) + const eventId = flagData.data.nostrEventId - if (!offer) { - throw new Error(`Offer ${p2wdbHash} not found in the database.`) + // Get the offer from the database. + const offer = await this.findOfferByEvent(eventId) + console.log(`Flagging this offer: ${JSON.stringify(offer, null, 2)}`) + + if (!offer) { + throw new Error(`Offer ${eventId} not found in the database.`) + } + + // Add the raw flag data to the database model. + offer.flags.push(flagData) + + // If flag count is 3 or more, mark the Offer as NSFW + const flagCnt = offer.flags.length + if (flagCnt >= 3) { + offer.nsfw = true + } + + // Save the updated offer data to the database. + await offer.save() + + return true + } catch (error) { + console.error('Error in flagOffer(): ', error) + throw error } - - // Add the raw flag data to the database model. - offer.flags.push(flagData) - - // If flag count is 3 or more, mark the Offer as NSFW - const flagCnt = offer.flags.length - if (flagCnt >= 3) { - offer.nsfw = true - } - - // Save the updated offer data to the database. - await offer.save() - - return true } // Get offers data from nostr. diff --git a/test/unit/mocks/adapters/index.js b/test/unit/mocks/adapters/index.js index fb4f9da..72e68d2 100644 --- a/test/unit/mocks/adapters/index.js +++ b/test/unit/mocks/adapters/index.js @@ -151,7 +151,11 @@ const wallet = { }, bchWallet: new MockBchWallet(), moveTokens: async () => {}, - reclaimTokens: async ()=>{} + moveBch: async () => {}, + reclaimTokens: async ()=>{}, + generatePartialTx: async ()=>{}, + deseralizeTx:async ()=>{}, + completeTx:async ()=>{ return ''}, } const p2wdb = { diff --git a/test/unit/mocks/adapters/wallet.js b/test/unit/mocks/adapters/wallet.js index 09e8378..70af917 100644 --- a/test/unit/mocks/adapters/wallet.js +++ b/test/unit/mocks/adapters/wallet.js @@ -24,6 +24,7 @@ class MockBchWallet { this.sendTokens = async () => { return 'fakeTxid'; }; + this.utxoIsValid =async ()=>{} this.getUtxos = async () => { }; this.getBalance = async () => { }; this.listTokens = async () => { }; diff --git a/test/unit/mocks/use-cases/offer-mock-data.js b/test/unit/mocks/use-cases/offer-mock-data.js index 87ec905..83fc742 100644 --- a/test/unit/mocks/use-cases/offer-mock-data.js +++ b/test/unit/mocks/use-cases/offer-mock-data.js @@ -3,45 +3,45 @@ */ const nftOffer01 = { - "messageType": 1, - "messageClass": 1, - "tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b", - "buyOrSell": "sell", - "rateInBaseUnit": 7672536, - "minUnitsToExchange": 7672536, - "numTokens": 1, - "utxoTxid": "c736d73e274df20e9b069b4990d1a264fb80aa98d67cc6c7a39e42bff48e7c04", - "utxoVout": 1, - "timestamp": 1662930309998, - "globaltimestamp": "2022-09-11T21:05:09.998Z", - "localTimestamp": "9/11/2022, 9:05:09 PM", - "txid": "4266862b8358664996038d8c29d49dc0f3d3058fd1ef3d567b084e6b16ceb5b2", - "p2wdbHash": "zdpuArq7rCuVCGPyTnWpLjy9AYsh8yrbwjGdKwg2GDDALkM8t", - "offerStatus": "posted", - "makerAddr": "bitcoincash:qrqlz63cwmu0hcmsrfnd8jemn3atkpaqds6tf4ksrr", - "ticker": "TV001", - "tokenType": 65 + "messageType": 1, + "messageClass": 1, + "tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b", + "buyOrSell": "sell", + "rateInBaseUnit": 7672536, + "minUnitsToExchange": 7672536, + "numTokens": 1, + "utxoTxid": "c736d73e274df20e9b069b4990d1a264fb80aa98d67cc6c7a39e42bff48e7c04", + "utxoVout": 1, + "timestamp": 1662930309998, + "globaltimestamp": "2022-09-11T21:05:09.998Z", + "localTimestamp": "9/11/2022, 9:05:09 PM", + "txid": "4266862b8358664996038d8c29d49dc0f3d3058fd1ef3d567b084e6b16ceb5b2", + "p2wdbHash": "zdpuArq7rCuVCGPyTnWpLjy9AYsh8yrbwjGdKwg2GDDALkM8t", + "offerStatus": "posted", + "makerAddr": "bitcoincash:qrqlz63cwmu0hcmsrfnd8jemn3atkpaqds6tf4ksrr", + "ticker": "TV001", + "tokenType": 65 } const nftTokenData01 = { - "genesisData": { - "type": 65, - "ticker": "TV001", - "name": "Introduction to NFTs on BCH", - "tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b", - "documentUri": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4", - "documentHash": "c1731268f4873f1928438abdaf6ffc546d86a1817dc6f3c6bc73fbdfb4664f10", - "decimals": 0, - "mintBatonIsActive": false, - "tokensInCirculationBN": "1", - "tokensInCirculationStr": "1", - "blockCreated": 740395, - "totalBurned": "0", - "totalMinted": "1", - "parentGroupId": "030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107" - }, - "immutableData": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4", - "mutableData": "ipfs://bafybeifzhunfpodsztsj5x4ypopngkxuapbxwaxaxkahaybdktk6joqtlq" + "genesisData": { + "type": 65, + "ticker": "TV001", + "name": "Introduction to NFTs on BCH", + "tokenId": "eb93f05553ff088bffb0ec687519e83c59e5108c160f7c25a4b6c45109d7e40b", + "documentUri": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4", + "documentHash": "c1731268f4873f1928438abdaf6ffc546d86a1817dc6f3c6bc73fbdfb4664f10", + "decimals": 0, + "mintBatonIsActive": false, + "tokensInCirculationBN": "1", + "tokensInCirculationStr": "1", + "blockCreated": 740395, + "totalBurned": "0", + "totalMinted": "1", + "parentGroupId": "030563ddd65772d8e9b79b825529ed53c7d27037507b57c528788612b4911107" + }, + "immutableData": "ipfs://bafybeibmgilu4lk3uivwyzhtm7jorl5zbsluewqmcsxs6otzrphh33okk4", + "mutableData": "ipfs://bafybeifzhunfpodsztsj5x4ypopngkxuapbxwaxaxkahaybdktk6joqtlq" } const simpleNftOffer01 = { @@ -126,7 +126,7 @@ const fungibleTokenData01 = { "mutableData": "" } -const offerMockData ={ +const offerMockData = { data: { messageType: 1, messageClass: 1, @@ -144,6 +144,31 @@ const offerMockData ={ } } +const deserealizeTxMock = { + //... + vout: [ + { + value: 0, + scriptPubKey: { + addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478'] + } + }, + { + value: 0, + scriptPubKey: { + addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478'] + } + }, + { + value: 0, + scriptPubKey: { + addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478'] + } + }, + //... + ] +} + export default { nftOffer01, nftTokenData01, @@ -151,5 +176,6 @@ export default { simpleNftTokenData01, fungibleOffer01, fungibleTokenData01, - offerMockData + offerMockData, + deserealizeTxMock }; diff --git a/test/unit/use-cases/offer.use-case.unit.js b/test/unit/use-cases/offer.use-case.unit.js index 76df883..03c07c3 100644 --- a/test/unit/use-cases/offer.use-case.unit.js +++ b/test/unit/use-cases/offer.use-case.unit.js @@ -191,6 +191,69 @@ describe('#offer-use-case', () => { assert.equal(result, false) }) + it('should handle error', async () => { + try { + // Mock dependencies and force desired code path. + sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error')) + // sandbox.stub(uut.OfferModel, 'remove').resolves() + + await uut.removeDuplicateOffers() + assert.fail('unexpected code path') + } catch (error) { + assert.equal(error.message, 'test error') + } + }) + }) + describe('#removeStaleOffers', () => { + it('remove offer with wrong utxoState', async () => { + // Mock dependencies and force desired code path. + sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }]) + sandbox.stub(uut.retryQueue, 'addToQueue').resolves(false) + + await uut.removeStaleOffers() + }) + it('remove offer with wrong txid', async () => { + // Mock dependencies and force desired code path. + sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }]) + sandbox.stub(uut.retryQueue, 'addToQueue').throws(new Error('txid needs to be a proper transaction ID')) + + await uut.removeStaleOffers() + }) + it('remove expired offer ', async () => { + const tsMock = new Date() + tsMock.setMonth(tsMock.getMonth() - 3) + + const timestamp = tsMock.getTime() + // Mock dependencies and force desired code path. + sandbox.stub(uut.OfferModel, 'find').resolves([{ timestamp, remove: async () => { } }]) + sandbox.stub(uut.retryQueue, 'addToQueue').resolves(true) + + await uut.removeStaleOffers() + }) + it('should handle axios error ', async () => { + const testErr = new Error() + testErr.isAxiosError = true + + // Mock dependencies and force desired code path. + sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }]) + sandbox.stub(uut.retryQueue, 'addToQueue').throws(testErr) + + await uut.removeStaleOffers() + }) + it('should handle error ', async () => { + try { + const testErr = new Error('unknow error') + + // Mock dependencies and force desired code path. + sandbox.stub(uut.OfferModel, 'find').resolves([{ remove: async () => { } }]) + sandbox.stub(uut.retryQueue, 'addToQueue').throws(testErr) + + await uut.removeStaleOffers() + assert.fail('unexpected code path') + } catch (error) { + assert.equal(error.message, 'unknow error') + } + }) }) describe('#findOfferByTxid', () => { it('should throw an error if input is not provided', async () => { @@ -256,4 +319,425 @@ describe('#offer-use-case', () => { assert.isFalse(result) }) }) + describe('#listOffers', () => { + it('should handle error', async () => { + try { + // Mock dependencies + sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error')) + + await uut.listOffers() + + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + + it('should list orders', async () => { + const queryMock = { + sort () { + return this + }, + skip () { + return this + }, + limit () { return [] } + } + // Mock dependencies + sandbox.stub(uut.OfferModel, 'find').returns(queryMock) + + const result = await uut.listOffers() + assert.isArray(result) + }) + }) + describe('#listNftOffers', () => { + it('should handle error', async () => { + try { + // Mock dependencies + sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error')) + + await uut.listNftOffers() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + + it('should list orders', async () => { + const queryMock = { + sort () { + return this + }, + skip () { + return this + }, + limit () { return [] } + } + // Mock dependencies + sandbox.stub(uut.OfferModel, 'find').returns(queryMock) + + const result = await uut.listNftOffers(1, true) + assert.isArray(result) + }) + }) + describe('#listFungibleOffers', () => { + it('should handle error', async () => { + try { + // Mock dependencies + sandbox.stub(uut.OfferModel, 'find').throws(new Error('test error')) + + await uut.listFungibleOffers() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + + it('should list orders', async () => { + const queryMock = { + sort () { + return this + }, + skip () { + return this + }, + limit () { return [] } + } + + // Mock dependencies + sandbox.stub(uut.OfferModel, 'find').returns(queryMock) + + const result = await uut.listFungibleOffers() + assert.isArray(result) + }) + }) + describe('#takeOffer', () => { + it('should handle error if input is not provided', async () => { + try { + await uut.takeOffer() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'eventId must be a string') + } + }) + it('should handle error for wrong offer status', async () => { + try { + // Mock dependencies + sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'completed' }) + + await uut.takeOffer('eventId') + + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'offer status is not "posted", so offer is dead and can not be countered.') + } + }) + it('should handle error for invalid utxo', async () => { + try { + // Mock dependencies + sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' }) + sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(false) + + await uut.takeOffer('eventId') + + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'UTXO does not exist. Aborting.') + } + }) + + it('should handle insufficient funds', async () => { + try { + // Mock dependencies + sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' }) + sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true) + sandbox.stub(uut, 'ensureFunds').throws(new Error('test error')) + + await uut.takeOffer('eventId') + + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should handle error if counter offer cant be calculated', async () => { + try { + // Mock dependencies + sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' }) + sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true) + sandbox.stub(uut, 'ensureFunds').resolves(true) + + await uut.takeOffer('eventId') + + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'Could not calculate the amount of BCH to generate counter offer') + } + }) + it('should handle error if counter offer cant be calculated', async () => { + try { + // Mock dependencies + sandbox.stub(uut, 'findOfferByEvent').returns({ offerStatus: 'posted' }) + sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true) + sandbox.stub(uut, 'ensureFunds').resolves(true) + + await uut.takeOffer('eventId') + + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'Could not calculate the amount of BCH to generate counter offer') + } + }) + it('should take offer', async () => { + // Mock data + const offerMock = Object.assign({}, mockData.offerMockData.data) + offerMock.remove = async () => { } + offerMock.offerStatus = 'posted' + + // Mock dependencies + sandbox.stub(uut, 'findOfferByEvent').returns(offerMock) + sandbox.stub(uut.adapters.wallet.bchWallet, 'utxoIsValid').returns(true) + sandbox.stub(uut, 'ensureFunds').resolves(true) + sandbox.stub(uut.adapters.wallet, 'moveBch').resolves({ sats: 1 }) + + await uut.takeOffer('eventId') + }) + }) + describe('#ensureFunds', () => { + it('should handle insufficient funds to use p2wdb', async () => { + try { + // Mock dependencies + sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(false) + + await uut.ensureFunds(mockData.offerMockData.data) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'App wallet does not have funds for writing to the P2WDB') + } + }) + + it('should throw error if sats needed could no be able to calculated', async () => { + try { + // Mock dependencies + sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true) + + // Mock Input + const mock = Object.assign({}, mockData.offerMockData.data) + mock.rateInBaseUnit = null + + await uut.ensureFunds(mock) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'Could not calculate sats needed!') + } + }) + + it('should throw error if app wallet does not have enough bch', async () => { + try { + // Mock dependencies + sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true) + sandbox.stub(uut.adapters.wallet.bchWallet, 'getBalance').resolves(0) + + await uut.ensureFunds(mockData.offerMockData.data) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'App wallet does not control enough BCH to purchase the tokens.') + } + }) + + it('should handle BUY offer', async () => { + try { + const mock = Object.assign({}, mockData.offerMockData.data) + mock.buyOrSell = 'buy' + + await uut.ensureFunds(mock) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'Buy offers are not supported yet.') + } + }) + it('should return true', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.p2wdb, 'checkForSufficientFunds').resolves(true) + sandbox.stub(uut.adapters.wallet.bchWallet, 'getBalance').resolves(10 * 10 ** 6) + + const result = await uut.ensureFunds(mockData.offerMockData.data) + assert.isTrue(result) + }) + }) + describe('#findOrderByEvent', () => { + it('should throw an error if hash is not provided', async () => { + try { + await uut.findOfferByEvent() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'nostrEventId must be a string') + } + }) + it('should throw an error if order is not found!', async () => { + try { + // Mock dependencies + sandbox.stub(uut.OfferModel, 'findOne').resolves(null) + + await uut.findOfferByEvent('eventId') + + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'offer not found') + } + }) + it('should return offer by eventId', async () => { + // Mock dependencies + sandbox.stub(uut.OfferModel, 'findOne').resolves({ toObject: () => { return { hash: 'hash' } } }) + + const result = await uut.findOfferByEvent('eventId') + assert.isObject(result) + }) + }) + describe('#flagOffer', () => { + it('should throw an error if input is not provided', async () => { + try { + await uut.flagOffer() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, '"data" property is required') + } + }) + it('should throw an error if offer is not found!', async () => { + try { + // Mock dependencies + sandbox.stub(uut, 'findOfferByEvent').resolves(null) + + const input = { + data: + { + nostrEventId: 'eventId' + } + } + await uut.flagOffer(input) + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'not found in the database') + } + }) + it('should flag offer', async () => { + // Mock dependencies + sandbox.stub(uut, 'findOfferByEvent').resolves({ flags: ['a', 'b', 'c'], save: () => { } }) + + const input = { + data: + { + nostrEventId: 'eventId' + } + } + const result = await uut.flagOffer(input) + assert.isTrue(result) + }) + }) + + describe('#loadOffers', () => { + it('should handle nostr error', async () => { + try { + // Mock dependencies + sandbox.stub(uut.adapters.nostr, 'read').throws(new Error('test error')) + + await uut.loadOffers() + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + it('should skip internal function errors ', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.nostr, 'read').resolves([mockData.offerMockData]) + + await uut.loadOffers() + }) + it('should review and load offers', async () => { + // Mock dependencies + sandbox.stub(uut.adapters.nostr, 'read').resolves([JSON.stringify(mockData.offerMockData)]) + + await uut.loadOffers() + }) + }) + describe('#acceptCounterOffer', () => { + it('should return if order is not found!', async () => { + // Mock dependencies + sandbox.stub(uut.orderUseCase, 'findOrderByEvent').throws(new Error('test error')) + + const result = await uut.acceptCounterOffer({ data: { /** .... */ } }) + assert.equal(result, 'N/A') + }) + it('should handle error if counter offer cant be calculated', async () => { + try { + // Mock Data + const mock = Object.assign({}, mockData.offerMockData.data) + mock.rateInBaseUnit = null + + // Mock dependencies + sandbox.stub(uut.orderUseCase, 'findOrderByEvent').resolves(mock) + + await uut.acceptCounterOffer({ data: { /** .... */ } }) + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'Could not calculate the amount of BCH offered in the Counter Offer') + } + }) + it('should handle error for wrong transaction output', async () => { + try { + // Mock dependencies + + 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) + + await uut.acceptCounterOffer({ data: { /** .... */ } }) + assert.fail('unexpected code path') + } catch (err) { + assert.include(err.message, 'The Counter Offer has an output of ') + assert.include(err.message, 'which does not match the required') + } + }) + it('should handle error for wrong transaction output address', async () => { + try { + // Mock data + const mock = Object.assign({}, mockData.offerMockData.data) + mock.makerAddr = 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7477' // Unknow Adress + mock.rateInBaseUnit = 0 + mock.numTokens = 0 + + // Mock dependencies + sandbox.stub(uut.orderUseCase, 'findOrderByEvent').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 (err) { + assert.include(err.message, 'The Counter Offer has an output address of') + assert.include(err.message, 'which does not match the Maker address') + } + }) + it('should return tx id', async () => { + // Mock data + const mock = Object.assign({}, mockData.offerMockData.data) + mock.makerAddr = mockData.deserealizeTxMock.vout[2].scriptPubKey.addresses[0] // Maker Address + mock.rateInBaseUnit = 0 + mock.numTokens = 0 + + // Mock dependencies + sandbox.stub(uut.orderUseCase, 'findOrderByEvent').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.isString(result) + }) + }) })