Compare commits

...
17 Commits
Author SHA1 Message Date
Chris Troutner ae6e708f51 Merge pull request #107 from Permissionless-Software-Foundation/ct-unstable
Reducing debug console logs
2025-09-16 10:22:09 -07:00
Chris Troutner 442f4ed752 Reducing more console logs 2025-09-16 10:04:00 -07:00
Chris Troutner 8617cf5fd0 fix(offers): Reducing console logs 2025-09-14 09:52:03 -07:00
Chris Troutner 2e00230f79 Merge branch 'master' into ct-unstable 2025-09-14 08:41:21 -07:00
Chris Troutner 44dcce6c45 Merge pull request #106 from Permissionless-Software-Foundation/dh-unit-test-coverage
feat(tests): Added more unit test coverage
2025-09-14 08:32:52 -07:00
Chris Troutner 8d43192a1c minor edits 2025-09-14 08:31:29 -07:00
Daniel Gonzalez 59e2bb473b feat(tests): Added more unit test coverage 2025-09-13 14:36:27 -04:00
Chris Troutner b4d2506e81 Tried AI generated architectural diagram 2025-09-10 06:50:35 -07:00
Chris Troutner 2af50ecf02 Merge pull request #105 from Permissionless-Software-Foundation/operator-fee
Adding required Operator fee to SWaP TX
2025-08-10 11:06:55 -07:00
Chris Troutner f88a24c1b5 Updating unit tests 2025-08-10 10:44:45 -07:00
Chris Troutner a4b4ff13d2 Handling corner cases when validating counter offer against operator fee 2025-08-09 17:54:30 -07:00
Chris Troutner 2bf4f9df28 fix(acceptCounterOffer()): Skipping Counter Offers that are missing an output for the operator 2025-08-09 16:28:56 -07:00
Chris Troutner 19ba38e8ce Adding operator fee to Offer use case, entity, and database model 2025-08-09 15:08:31 -07:00
Chris Troutner 373a52ebf1 feat(operator fee): Adding config values to ensure bch-dex operator recieves a fee 2025-08-09 15:02:29 -07:00
Chris Troutner fe82478dc4 linting 2025-08-09 14:49:41 -07:00
Chris Troutner bed281c8c3 minor edit 2025-08-09 14:49:32 -07:00
Chris Troutner 01484b4e03 fixing typo in comments 2025-08-07 07:00:58 -07:00
32 changed files with 1683 additions and 83 deletions
+5 -1
View File
@@ -191,5 +191,9 @@ export default {
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
// Admin password
adminPassword: process.env.ADMIN_PASSWORD
adminPassword: process.env.ADMIN_PASSWORD,
// The Operator of BCH DEX can elect to receive a percentage of each sale.
operatorAddress: process.env.OPERATOR_ADDRESS ? process.env.OPERATOR_ADDRESS : 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
operatorPercentage: process.env.OPERATOR_PERCENTAGE ? parseFloat(process.env.OPERATOR_PERCENTAGE) : 2.0
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+4 -1
View File
@@ -35,8 +35,11 @@ const Offer = new mongoose.Schema({
lokadId: { type: String },
messageType: { type: Number },
messageClass: { type: Number },
nostrEventId: { type: String } // Nostr Event Id.
nostrEventId: { type: String }, // Nostr Event Id.
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number }
})
export default mongoose.model('offer', Offer)
+5 -1
View File
@@ -43,7 +43,11 @@ const Order = new mongoose.Schema({
// Additional properties found in createOrder
dataType: { type: String, required: true },
userId: { type: String, required: true }
userId: { type: String, required: true },
// Operator fee and address
operatorAddress: { type: String },
operatorPercentage: { type: Number }
})
export default mongoose.model('order', Order)
@@ -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
@@ -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.'
)
}
@@ -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
+1
View File
@@ -105,6 +105,7 @@ class TimerControllers {
console.error('Error in time-controller.js/cleanUsage(): ', err)
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
return false
}
}
+3
View File
@@ -1,6 +1,9 @@
/*
Entry Entity
TODO: Explain what an entry is and how it works.
*/
class Entry {
validate ({
entry,
+12 -2
View File
@@ -1,6 +1,6 @@
/*
Offer Entity
An ffer is created when a new Signal is detected via the P2WDB webhook.
An offer is created when a new Signal is detected via the P2WDB webhook.
It's destroyed when the UTXO described in the Signal has been detected as spent.
*/
@@ -33,7 +33,9 @@ class OfferEntity {
makerAddr,
ticker,
tokenType,
nostrEventId
nostrEventId,
operatorAddress,
operatorPercentage
} = offerData.data
// Input Validation
@@ -80,6 +82,14 @@ class OfferEntity {
throw new Error("Property 'nostrEventId' must be a string.")
}
// Add the operator address and percentage to the offer.
if (!operatorAddress || typeof operatorAddress !== 'string') {
throw new Error("Property 'operatorAddress' must be a string.")
}
if (!operatorPercentage || typeof operatorPercentage !== 'number') {
throw new Error("Property 'operatorPercentage' must be a number.")
}
// Convert the timestamp to a number.
let timestamp = new Date(offerData.timestamp)
timestamp = timestamp.getTime()
+7 -1
View File
@@ -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) {
+48 -6
View File
@@ -111,7 +111,10 @@ class OfferUseCases {
const utxoStatus = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.utxoIsValid, utxo)
// console.log('utxoStatus: ', utxoStatus)
// if (utxoStatus === null) return false
if (!utxoStatus) return false
if (!utxoStatus) {
console.log(`UTXO txid: ${offerObj.data.utxoTxid}, vout: ${offerObj.data.utxoVout} has been spent. Skipping.`)
return false
}
// A new offer gets a status of 'posted'
offerObj.data.offerStatus = 'posted'
@@ -513,9 +516,8 @@ class OfferUseCases {
// }
}
// This function is called by the P2WDB webhook REST API handler. When a
// Counter Offer is passed to bch-dex by the P2WDB, the data is then passed
// to this function. It does due diligence on the Counter Offer, then signs
// This function is called by loadOffers().
// It does due diligence on the Counter Offer, then signs
// and broadcasts the transaction to accept the Counter Offer.
async acceptCounterOffer (offerData) {
try {
@@ -528,6 +530,8 @@ class OfferUseCases {
return false
}
console.log(`New counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerData.data.nostrEventId)}`)
// See if this instance of bch-dex is managing the Order associated with
// the incoming Counter Offer.
@@ -594,20 +598,57 @@ 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.`)
}
// Ensure the Counter Offer has an output for the Operator of bch-dex.
if (!txObj.vout[3]) {
console.log('The Counter Offer does not have an output for the Operator.')
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// 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 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
console.log('operatorSatsOut: ', operatorSatsOut, 'estimatedOperatorFee: ', estimatedOperatorFee)
if (operatorSatsOut < estimatedOperatorFee) {
console.log(`Skipping: The Counter Offer has an output of ${operatorSatsOut}, which is less than the estimated operator fee of ${estimatedOperatorFee}.`)
// Add order to list of seen orders, so that we don't spent time trying to validate it again.
this.seenOffers.push(eventId)
return 'N/A'
}
// Get the User ID from the Order model.
const userId = orderData.userId
@@ -718,7 +759,8 @@ class OfferUseCases {
await thisOffer.remove()
}
// If the Offer is older than 7 days, delete it.
// TODO: Instead of deleting the offer, send the token back to the Makers wallet.
// If the Offer is older than a threshold, delete it.
const nowMs = now.getTime()
const eightWeeks = 1000 * 60 * 60 * 24 * 7 * 8
const eightWeeksAgo = nowMs - eightWeeks
@@ -795,7 +837,7 @@ class OfferUseCases {
if (offerObj.data.dataType === 'counter-offer') {
// console.log('Counter offer detected: ', offerObj)
// console.log('Counter offer detected: ', offerObj.data.nostrEventId)
console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
// console.log(`Counter offer detected: https://astral.psfoundation.info/${this.adapters.nostr.eventId2note(offerObj.data.nostrEventId)}`)
await this.acceptCounterOffer(offerObj)
}
+7 -7
View File
@@ -44,6 +44,7 @@ class OrderLib {
if (!entryObj.tokenId) throw new Error('entry does not contain required properties')
// Get the user data from the database, for the user creating the new Order.
const user = await this.UserModel.findById(entryObj.userId)
if (!user) throw new Error('user not found')
@@ -81,13 +82,7 @@ class OrderLib {
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.optimize, {})
await userWallet.optimize()
// Ensure sufficient tokens exist to create the order.
// await this.ensureFunds(orderEntity)
// await this.retryQueue.addToQueue(this.ensureFunds, orderEntity)
// Get Ticker for token ID.
// const tokenData = await this.adapters.wallet.bchWallet.getTxData([entryObj.tokenId])
// const tokenData = await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.getTxData, [entryObj.tokenId])
const tokenData = await userWallet.getTxData([entryObj.tokenId])
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
orderEntity.ticker = tokenData[0].tokenTicker
@@ -107,15 +102,20 @@ class OrderLib {
// await this.adapters.wallet.bchWallet.getUtxos()
// await this.retryQueue.addToQueue(this.adapters.wallet.bchWallet.initialize, {})
await userWallet.initialize()
// Update the order with the new UTXO information.
orderEntity.utxoTxid = utxoInfo.txid
orderEntity.utxoVout = utxoInfo.vout
orderEntity.tokenType = utxoInfo.tokenType
// Add P2WDB specific flag for signaling that this is a new offer.
// Add flag to signal that this is a new offer.
orderEntity.dataType = 'offer'
orderEntity.userId = user._id
// Add the operator address and percentage to the order.
orderEntity.operatorAddress = this.config.operatorAddress
orderEntity.operatorPercentage = this.config.operatorPercentage
// Post the new Order information to Nostr under the topic set in the
// config file.
const postObj = {
+3 -1
View File
@@ -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
@@ -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', () => {
@@ -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 {
@@ -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)
})
})
})
@@ -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')
}
})
})
})
})
@@ -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.'
)
}
})
})
})
@@ -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')
}
})
})
})
@@ -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.'
)
}
})
})
})
@@ -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 () => {
@@ -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)
})
})
})
+228 -1
View File
@@ -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',
@@ -286,7 +511,9 @@ describe('#Offer-Entity', () => {
makerAddr: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
ticker: 'TROUT',
tokenType: 1,
nostrEventId: 'test'
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
operatorPercentage: 10
},
timestamp: '2021-09-20T17:54:26.395Z',
localTimeStamp: '9/20/2021, 10:54:26 AM',
+66
View File
@@ -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)
})
})
})
+15 -1
View File
@@ -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}
+42
View File
@@ -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 }
+46
View File
@@ -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;
+36 -1
View File
@@ -142,10 +142,38 @@ const offerMockData = {
utxoVout: 0,
makerAddr: 'address',
tokenType: 1,
nostrEventId: 'test'
nostrEventId: 'test',
operatorAddress: 'bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478',
operatorPercentage: 10
}
}
const deserealizeTxMockNoOperatorOut = {
//...
vout: [
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
const deserealizeTxMock = {
//...
vout: [
@@ -167,6 +195,12 @@ const deserealizeTxMock = {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
{
value: 0,
scriptPubKey: {
addresses: ['bitcoincash:qzy97glp47ut7tstm5g0tlrmkhk742795gkmyc7478']
}
},
//...
]
}
@@ -179,5 +213,6 @@ export default {
fungibleOffer01,
fungibleTokenData01,
offerMockData,
deserealizeTxMockNoOperatorOut,
deserealizeTxMock
};
+175 -12
View File
@@ -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)
@@ -746,6 +848,44 @@ describe('#offer-use-case', () => {
}
})
it('should skip transactions that do not have an output for the operator', async () => {
// 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, 'findOrderByUtxo').resolves(mock)
sandbox.stub(uut.adapters.wallet.bchWallet.bchjs.BitcoinCash, 'toSatoshi').returns(0)
sandbox.stub(uut.adapters.wallet, 'deseralizeTx').resolves(mockData.deserealizeTxMockNoOperatorOut)
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 {
// Mock data
@@ -761,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')
@@ -769,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')
})
})
})
@@ -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,
@@ -185,6 +186,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should create an order with consumer-api', async () => {
uut.config.useFullStackCash = false
const entryObj = {
@@ -214,6 +216,7 @@ describe('#order-use-case', () => {
assert.property(result, 'eventId')
assert.property(result, 'noteId')
})
it('should throw error if user is not found', async () => {
try {
const entryObj = {
@@ -384,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)
})
})
})
+166
View File
@@ -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')
}
})
})
})