mirror of
https://github.com/Permissionless-Software-Foundation/ipfs-bch-wallet-service.git
synced 2026-09-22 01:02:03 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff6a032bbf | ||
|
|
3f38440841 | ||
|
|
e9606ecfe8 | ||
|
|
bfc0a5be9f | ||
|
|
af68e411bd | ||
|
|
b9b79c2142 | ||
|
|
efb02a4381 | ||
|
|
d894c1e596 | ||
|
|
821cf4abe8 | ||
|
|
74696cfb9e | ||
|
|
0bfb84e21b | ||
|
|
35b4e7079d | ||
|
|
cebe7e14c2 | ||
|
|
e0de9aa729 | ||
|
|
75ea6ecdde | ||
|
|
d4c9665c16 | ||
|
|
0855959a35 | ||
|
|
3b8f275526 | ||
|
|
768bfdb4e7 | ||
|
|
764fe928d2 | ||
|
|
2de61458d3 | ||
|
|
7433446581 | ||
|
|
d38e371a05 | ||
|
|
a68ca92468 | ||
|
|
96b3a98929 | ||
|
|
dc3ef76e00 | ||
|
|
7c26e33a8c | ||
|
|
2595ee8295 | ||
|
|
476fdcd0aa | ||
|
|
b6762b215e | ||
|
|
0e6a411012 | ||
|
|
55e9e35c1f | ||
|
|
18ea03a4d6 | ||
|
|
3d911d1062 | ||
|
|
cf88fadfdc | ||
|
|
4977ae7003 | ||
|
|
f0f6970a08 | ||
|
|
fcb4acb11d | ||
|
|
28e012feb6 | ||
|
|
958c637c24 | ||
|
|
34381e6e93 | ||
|
|
8843b3e2b0 | ||
|
|
3f91653d23 | ||
|
|
3a4569c33f | ||
|
|
05a0620663 | ||
|
|
af83c06b43 | ||
|
|
e6317c3282 | ||
|
|
354321c956 | ||
|
|
af2572a493 | ||
|
|
80c416a2dd | ||
|
|
49f61ade1f | ||
|
|
69a3406bd8 | ||
|
|
c65d662630 | ||
|
|
1b99f8afa1 | ||
|
|
c6f0a1317c | ||
|
|
fad5f8e23d | ||
|
|
ec6e0f5e46 | ||
|
|
f1caef2c0d | ||
|
|
55bf9424f9 | ||
|
|
aa100c97ed | ||
|
|
9fa5eafe95 | ||
|
|
4d9e948e30 | ||
|
|
e5cdf28e34 | ||
|
|
1d82b36105 | ||
|
|
b910cd1e6d | ||
|
|
536861ba99 | ||
|
|
ef030e3dc6 | ||
|
|
3da46c3d6f | ||
|
|
82f5552836 | ||
|
|
4d25681c51 | ||
|
|
98a1631498 | ||
|
|
8dce8cbf8d | ||
|
|
0b3049138b | ||
|
|
d7136583b8 | ||
|
|
c721da1562 | ||
|
|
3606a2d31b | ||
|
|
764905b041 | ||
|
|
556c6ec522 | ||
|
|
04395b70a6 | ||
|
|
81521151f4 | ||
|
|
5a819c1d9c | ||
|
|
f2f3208fda | ||
|
|
7fe888cdda | ||
|
|
db3f1bf2cf | ||
|
|
8027eba6ef | ||
|
|
84422e8727 | ||
|
|
5f64257dca | ||
|
|
2fbdccb883 | ||
|
|
ecc10afac3 | ||
|
|
dd69259908 | ||
|
|
1fb4ff1947 | ||
|
|
2d02f1c573 | ||
|
|
4fc54aa9c5 | ||
|
|
2bd7f0c45c | ||
|
|
900423339e | ||
|
|
d907afa6bb | ||
|
|
9b116d82b7 | ||
|
|
1d1bf50ee3 | ||
|
|
5002004697 | ||
|
|
f311a3b9c6 | ||
|
|
fe91f4b875 | ||
|
|
2d9a2b811c | ||
|
|
514b65ed24 | ||
|
|
0c7f3088a8 | ||
|
|
41235b5919 | ||
|
|
12cab02300 | ||
|
|
04a908261a | ||
|
|
4854eae545 | ||
|
|
a3ceb872a5 | ||
|
|
0390ce7e29 | ||
|
|
c571423326 | ||
|
|
e8d0f1fcb8 | ||
|
|
57f49ff31f | ||
|
|
8b04a54fe1 | ||
|
|
88135669cb | ||
|
|
83898742f1 | ||
|
|
1795918719 | ||
|
|
6894217078 | ||
|
|
3ffbed27b5 | ||
|
|
b372e3d260 | ||
|
|
707b385b8a | ||
|
|
6cb140a491 | ||
|
|
c43ee8c25a | ||
|
|
0a7ec11510 | ||
|
|
80b9d17b67 | ||
|
|
f7aaba71d1 | ||
|
|
418a2e2ad2 | ||
|
|
aeb234869f | ||
|
|
a1023e8224 | ||
|
|
a32ed63d47 | ||
|
|
934fb46ffc | ||
|
|
b27136d03b | ||
|
|
064ac36dc7 | ||
|
|
5e2cf86f4b | ||
|
|
425d2ada65 | ||
|
|
60a3d67c0b | ||
|
|
a03c5d8c91 | ||
|
|
bcc90da49b | ||
|
|
b473a3803c | ||
|
|
9614f186b4 | ||
|
|
0390f2d956 | ||
|
|
bff9db0e8f | ||
|
|
12f746ae80 | ||
|
|
e590a9e5f0 | ||
|
|
0f0964e367 | ||
|
|
9a4379c03e | ||
|
|
ba5ead998d | ||
|
|
409a0f1049 | ||
|
|
bd2f27bb64 | ||
|
|
55d8d131cf | ||
|
|
5a6e3247e8 | ||
|
|
2a522775eb | ||
|
|
d677c83795 | ||
|
|
67b61fc80c | ||
|
|
9509634651 | ||
|
|
62ac11c861 | ||
|
|
316c5d6399 | ||
|
|
e1fbd6bce9 | ||
|
|
7cffd9f377 | ||
|
|
0a406cb9a4 | ||
|
|
53d84300ec | ||
|
|
3fb0379175 | ||
|
|
f172a51016 | ||
|
|
00d53659b1 | ||
|
|
b0797d92f5 | ||
|
|
48484ed7af | ||
|
|
6a4aadde8a | ||
|
|
79e3aba26e | ||
|
|
bf61359bc7 |
@@ -72,3 +72,5 @@ data/
|
||||
run-dev.sh
|
||||
|
||||
!README.md
|
||||
|
||||
wallet.json
|
||||
|
||||
@@ -24,9 +24,9 @@ This repository is one step in a longer path. Here are the major milestones of t
|
||||
|
||||
## Requirements
|
||||
|
||||
- node **^16.20.2**
|
||||
- npm **^8.19.4**
|
||||
- Docker **^20.10.8**
|
||||
- node **^20.16.0**
|
||||
- npm **^10.8.1**
|
||||
- Docker **^24.0.7**
|
||||
- Docker Compose **^1.27.4**
|
||||
|
||||
## Installation
|
||||
@@ -61,18 +61,19 @@ docker-compose up -d
|
||||
- You can bring the containers down with `docker-compose down`
|
||||
- You can bring the containers back up with `docker-compose up -d`.
|
||||
|
||||
### Development Environment
|
||||
### Configuration
|
||||
|
||||
A development environment will allow you modify the code on-the-fly and contribute to the code base of this repository. Ubuntu v20 is the recommended OS for creating a dev environment. Other operating systems may cause issues.
|
||||
This program is intended to started with a Bash shell script. In that script, you can set the following environment variables:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Permissionless-Software-Foundation/ipfs-service-provider
|
||||
cd ipfs-service-provider
|
||||
./install-mongo-sh
|
||||
npm install
|
||||
npm start
|
||||
# The instance of bch-api to connect to.
|
||||
export APISERVER=http://172.17.0.1:3000/v5/
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
This app is intended to be started via a bash shell script. See the environment variables used to configure this app in the [config/env/common.js file](./config/env/common.js).
|
||||
|
||||
## File Structure
|
||||
|
||||
The file layout of this repository follows the file layout of [Clean Architecture](https://christroutner.github.io/trouts-blog/blog/clean-architecture). Understaning the principles laid out this article will help developers navigate the code base.
|
||||
@@ -106,7 +107,7 @@ There is additional developer documentation in the [dev-docs directory](./dev-do
|
||||
- [Mocha](https://mochajs.org/)
|
||||
- [apidoc](http://apidocjs.com/)
|
||||
- [ESLint](http://eslint.org/)
|
||||
- [ipfs-coord](https://www.npmjs.com/package/ipfs-coord)
|
||||
- [helia-coord](https://www.npmjs.com/package/helia-coord)
|
||||
|
||||
## IPFS
|
||||
|
||||
|
||||
+25
-19
@@ -25,6 +25,7 @@ import config from '../config/index.js' // this first.
|
||||
|
||||
import AdminLib from '../src/adapters/admin.js'
|
||||
import errorMiddleware from '../src/controllers/rest-api/middleware/error.js'
|
||||
import { usageMiddleware } from '../src/use-cases/usage-use-cases.js'
|
||||
import wlogger from '../src/adapters/wlogger.js'
|
||||
import Controllers from '../src/controllers/index.js'
|
||||
import { applyPassportMods } from '../config/passport.js'
|
||||
@@ -45,16 +46,18 @@ class Server {
|
||||
const app = new Koa()
|
||||
app.keys = [this.config.session]
|
||||
|
||||
// Connect to the Mongo Database.
|
||||
this.mongoose.Promise = global.Promise
|
||||
this.mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
console.log(
|
||||
`Connecting to MongoDB with this connection string: ${this.config.database}`
|
||||
)
|
||||
await this.mongoose.connect(this.config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
if (!this.config.noMongo) {
|
||||
// Connect to the Mongo Database.
|
||||
this.mongoose.Promise = global.Promise
|
||||
this.mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
console.log(
|
||||
`Connecting to MongoDB with this connection string: ${this.config.database}`
|
||||
)
|
||||
await this.mongoose.connect(this.config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`Starting environment: ${this.config.env}`)
|
||||
console.log(`Debug level: ${this.config.debugLevel}`)
|
||||
@@ -65,6 +68,7 @@ class Server {
|
||||
app.use(bodyParser())
|
||||
app.use(session())
|
||||
app.use(errorMiddleware())
|
||||
app.use(usageMiddleware())
|
||||
|
||||
// Used to generate the docs.
|
||||
app.use(mount('/', serve(`${process.cwd()}/docs`)))
|
||||
@@ -102,14 +106,14 @@ class Server {
|
||||
this.server = await app.listen(this.config.port)
|
||||
console.log(`Server started on ${this.config.port}`)
|
||||
|
||||
// Create the system admin user.
|
||||
try {
|
||||
const success = await this.adminLib.createSystemUser()
|
||||
if (success) console.log('System admin user created.')
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Error trying to create system admin. Perhaps one already exists?'
|
||||
)
|
||||
if (!this.config.noMongo) {
|
||||
try {
|
||||
// Create the system admin user.
|
||||
const success = await this.adminLib.createSystemUser()
|
||||
if (success) console.log('System admin user created.')
|
||||
} catch (err) {
|
||||
console.log('Could not create system admin user.')
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the other IPFS controllers.
|
||||
@@ -122,7 +126,9 @@ class Server {
|
||||
console.log('\nConfiguration:')
|
||||
console.log(`Circuit Relay: ${this.config.isCircuitRelay}`)
|
||||
console.log(`IPFS TCP port: ${this.config.ipfsTcpPort}`)
|
||||
console.log(`IPFS WS port: ${this.config.ipfsWsPort}\n`)
|
||||
console.log(`IPFS WS port: ${this.config.ipfsWsPort}`)
|
||||
console.log(`IPFS WebRTC port: ${this.config.ipfsWebRtcPort}`)
|
||||
console.log(`Connection preference: ${this.config.connectPref}\n`)
|
||||
|
||||
return app
|
||||
} catch (err) {
|
||||
|
||||
Vendored
+63
-15
@@ -18,7 +18,6 @@ import * as url from 'url'
|
||||
import { readFileSync } from 'fs'
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
const pkgInfo = JSON.parse(readFileSync(`${__dirname.toString()}/../../package.json`))
|
||||
|
||||
const version = pkgInfo.version
|
||||
|
||||
const ipfsCoordName = process.env.COORD_NAME
|
||||
@@ -29,7 +28,7 @@ console.log('GET_JWT_AT_STARTUP: ', process.env.GET_JWT_AT_STARTUP)
|
||||
|
||||
export default {
|
||||
// Configure TCP port.
|
||||
port: process.env.PORT || 5020,
|
||||
port: process.env.PORT || 5032,
|
||||
|
||||
// Password for HTML UI that displays logs.
|
||||
logPass: 'test',
|
||||
@@ -45,14 +44,46 @@ export default {
|
||||
? process.env.EMAILPASS
|
||||
: 'emailpassword',
|
||||
|
||||
// Enable or Disable the usage of Mongo DB.
|
||||
noMongo: process.env.NO_MONGO ? true : false,
|
||||
|
||||
// BEGIN WALLET CONFIGURATION
|
||||
|
||||
// BCH Mnemonic for generating encryption keys and payment address
|
||||
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
|
||||
|
||||
// (Optional) use a wallet file generated by psf-bch-wallet.
|
||||
walletFile: process.env.WALLET_FILE
|
||||
? process.env.WALLET_FILE
|
||||
: `${__dirname.toString()}/../../wallet.json`,
|
||||
|
||||
// Wallet Configuration
|
||||
// Default value: web3 - connect to bch-api through web 3 JSON RPC over IPFS.
|
||||
// Alternate value: web2 - connect to bch-api through a web 2 REST API over HTTP.
|
||||
walletInterface: process.env.WALLET_INTERFACE ? process.env.WALLET_INTERFACE : 'web3',
|
||||
|
||||
// Wallet API Server
|
||||
// Default web2 server: https://api.fullstack.cash/v5/
|
||||
// Default web3 server: https://free-bch.fullstack.cash/
|
||||
apiServer: process.env.WALLET_INTERFACE === 'web2'
|
||||
? (
|
||||
process.env.APISERVER
|
||||
? process.env.APISERVER
|
||||
: 'https://api.fullstack.cash/v5/'
|
||||
)
|
||||
: 'https://free-bch.fullstack.cash/',
|
||||
|
||||
// Basic Authentication Password for connecting to bch-api through a web 2
|
||||
// REST API over HTTP.
|
||||
authPass: process.env.WALLET_AUTH_PASS
|
||||
? process.env.WALLET_AUTH_PASS
|
||||
: '',
|
||||
|
||||
// FullStack.cash account information, used for automatic JWT handling.
|
||||
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : false,
|
||||
authServer: process.env.AUTHSERVER
|
||||
? process.env.AUTHSERVER
|
||||
: 'https://auth.fullstack.cash',
|
||||
apiServer: process.env.APISERVER
|
||||
? process.env.APISERVER
|
||||
: 'https://api.fullstack.cash/v5/',
|
||||
fullstackLogin: process.env.FULLSTACKLOGIN
|
||||
? process.env.FULLSTACKLOGIN
|
||||
: 'demo@demo.com',
|
||||
@@ -60,8 +91,17 @@ export default {
|
||||
? process.env.FULLSTACKPASS
|
||||
: 'demo',
|
||||
|
||||
// IPFS settings.
|
||||
useIpfs: process.env.DISABLE_IPFS ? false : true, // Disable IPFS flag
|
||||
// END WALLET CONFIGURATION
|
||||
|
||||
// BEGIN IPFS CONFIGURATION
|
||||
|
||||
// Debug verbosity level of helia-coord.
|
||||
// 0 = no debug logs. 3 = maximum logs.
|
||||
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 2,
|
||||
|
||||
// Enable/Disable the IPFS node at startup. Enabled by default.
|
||||
useIpfs: process.env.DISABLE_IPFS ? false : true,
|
||||
|
||||
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
|
||||
// SSL domain used for websocket connection via browsers.
|
||||
crDomain: process.env.CR_DOMAIN ? process.env.CR_DOMAIN : '',
|
||||
@@ -77,7 +117,7 @@ export default {
|
||||
'@type': 'WebAPI',
|
||||
name: ipfsCoordName,
|
||||
version,
|
||||
protocol: 'bch-wallet',
|
||||
protocol: 'ipfs-bch-wallet-service',
|
||||
description:
|
||||
'IPFS service providing BCH blockchain access needed by a wallet.',
|
||||
documentation: 'https://ipfs-bch-wallet-service.fullstack.cash/',
|
||||
@@ -85,17 +125,17 @@ export default {
|
||||
'@type': 'Organization',
|
||||
name: 'Permissionless Software Foundation',
|
||||
url: 'https://PSFoundation.cash'
|
||||
}
|
||||
},
|
||||
|
||||
// If this node has an IP4 address or domain name used to provide a REST API.
|
||||
web2Api: process.env.WEB2_API ? process.env.WEB2_API : null
|
||||
},
|
||||
|
||||
// IPFS Ports
|
||||
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 5668,
|
||||
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 5669,
|
||||
|
||||
// BCH Mnemonic for generating encryption keys and payment address
|
||||
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
|
||||
|
||||
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 2,
|
||||
ipfsWebRtcPort: process.env.IPFS_WEB_RTC_PORT ? process.env.IPFS_WEB_RTC_PORT : 5670,
|
||||
connectPref: process.env.CONNECT_PREF, // Used in helia-coord to select connection preference.
|
||||
|
||||
// Settings for production, using external go-ipfs node.
|
||||
isProduction: process.env.SVC_ENV === 'prod' ? true : false,
|
||||
@@ -113,5 +153,13 @@ export default {
|
||||
|
||||
// v2 Circuit Relay server (FullStack.cash)
|
||||
// '/ip4/78.46.129.7/tcp/4001/p2p/12D3KooWFQ11GQ5NubsJGhYZ4X3wrAGimLevxfm6HPExCrMYhpSL'
|
||||
]
|
||||
],
|
||||
|
||||
// END IPFS CONFIGURATION
|
||||
|
||||
// Account Configuration
|
||||
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
|
||||
|
||||
// Admin password
|
||||
adminPassword: process.env.ADMIN_PASSWORD
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -12,6 +12,6 @@ export default {
|
||||
token: 'secret-jwt-token',
|
||||
database: process.env.DBURL
|
||||
? process.env.DBURL
|
||||
: 'mongodb://172.17.0.1:5555/bch-service-prod',
|
||||
: 'mongodb://172.17.0.1:5557/bch-service-prod',
|
||||
env: 'prod'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import Server from './bin/server.js'
|
||||
const server = new Server()
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.log(`Handling ${reason.code} error. stack: `, reason)
|
||||
})
|
||||
|
||||
server.startServer()
|
||||
|
||||
Generated
+9852
-12349
File diff suppressed because it is too large
Load Diff
+16
-24
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "ipfs-bch-wallet-service",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"description": "IPFS and REST service providing blockchain access needed by a wallet. ",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "npm run test:all",
|
||||
"test": "npm run test:unit",
|
||||
"test:all": "export SVC_ENV=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
|
||||
"test:unit": "export SVC_ENV=test && mocha --exit --timeout 15000 --recursive test/unit/",
|
||||
"test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 30000 test/e2e/automated/",
|
||||
"test:integration": "export SVC_ENV=test && mocha --timeout 30000 --recursive test/integration/",
|
||||
"test:unit": "export SVC_ENV=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit/",
|
||||
"test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/",
|
||||
"start:e2e:server": "export SVC_ENV=test && node index.js",
|
||||
"test:temp": "export SVC_ENV=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
|
||||
@@ -25,26 +25,10 @@
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/ipfs-bch-wallet-service",
|
||||
"dependencies": {
|
||||
"@chainsafe/libp2p-gossipsub": "11.0.1",
|
||||
"@chainsafe/libp2p-noise": "14.1.0",
|
||||
"@chainsafe/libp2p-yamux": "6.0.1",
|
||||
"@helia/unixfs": "3.0.1",
|
||||
"@libp2p/bootstrap": "10.0.7",
|
||||
"@libp2p/circuit-relay-v2": "1.0.13",
|
||||
"@libp2p/identify": "1.0.6",
|
||||
"@libp2p/keychain": "4.0.7",
|
||||
"@libp2p/logger": "4.0.5",
|
||||
"@libp2p/mplex": "10.0.16",
|
||||
"@libp2p/tcp": "9.0.7",
|
||||
"@libp2p/webrtc": "4.0.17",
|
||||
"@psf/bch-js": "6.7.3",
|
||||
"axios": "0.27.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blockstore-fs": "1.1.8",
|
||||
"datastore-fs": "9.1.6",
|
||||
"glob": "7.1.6",
|
||||
"helia": "2.1.0",
|
||||
"helia-coord": "1.5.5",
|
||||
"helia-coord": "2.0.3",
|
||||
"jsonrpc-lite": "2.2.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwt-bch-lib": "1.3.0",
|
||||
@@ -59,9 +43,8 @@
|
||||
"koa-router": "10.0.0",
|
||||
"koa-static": "5.0.0",
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "1.2.1",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "5.11.1",
|
||||
"minimal-slp-wallet": "7.1.4",
|
||||
"mongoose": "5.13.14",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.7.5",
|
||||
@@ -100,5 +83,14 @@
|
||||
"ignore": [
|
||||
"/test/unit/mocks/**/*.js"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"@multiformats/multiaddr-to-uri": "10.1.2",
|
||||
"@multiformats/multiaddr-matcher": "1.6.0",
|
||||
"@libp2p/utils": "6.7.1",
|
||||
"helia": {
|
||||
"libp2p": "2.7.2",
|
||||
"@libp2p/upnp-nat": "3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#
|
||||
|
||||
#IMAGE BUILD COMMANDS
|
||||
FROM ubuntu:20.04
|
||||
# ct-base-ubuntu = ubuntu 18.04 + nodejs v10 LTS
|
||||
#FROM christroutner/ct-base-ubuntu
|
||||
FROM ubuntu:24.04
|
||||
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||
|
||||
#Update the OS and install any OS packages needed.
|
||||
@@ -13,7 +15,7 @@ RUN apt-get install -y sudo git curl nano gnupg wget
|
||||
RUN sudo apt-get install -y ca-certificates curl gnupg
|
||||
RUN sudo mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN sudo apt-get update
|
||||
RUN sudo apt-get install -y nodejs build-essential
|
||||
|
||||
@@ -35,7 +37,7 @@ RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
||||
|
||||
# Update to the latest version of npm.
|
||||
# Working with npm@7.21.1
|
||||
RUN npm install -g npm
|
||||
#RUN npm install -g npm
|
||||
|
||||
# Update to the latest version of npm.
|
||||
#RUN npm install -g npm@7.23.0
|
||||
@@ -58,7 +60,7 @@ RUN git clone https://github.com/Permissionless-Software-Foundation/ipfs-bch-wal
|
||||
WORKDIR /home/safeuser/ipfs-bch-wallet-service
|
||||
|
||||
# For development: switch to unstable branch
|
||||
RUN git checkout ct-unstable
|
||||
RUN git checkout master
|
||||
|
||||
# Install dependencies
|
||||
#RUN npm install -g @mapbox/node-pre-gyp
|
||||
@@ -73,8 +75,6 @@ RUN mkdir /home/safeuser/.ipfsdata
|
||||
#VOLUME /home/safeuser/ipfsdata
|
||||
#VOLUME /home/safeuser/orbitdb
|
||||
|
||||
EXPOSE 5010
|
||||
|
||||
# Start the application.
|
||||
#COPY start-production.sh start-production.sh
|
||||
CMD ["./start-production.sh"]
|
||||
|
||||
@@ -7,16 +7,16 @@ services:
|
||||
image: mongo:4.2.0
|
||||
container_name: mongo-bch-service
|
||||
ports:
|
||||
- '5555:27017' # <host port>:<container port>
|
||||
- '5557:27017' # <host port>:<container port>
|
||||
volumes:
|
||||
- ../data/mongodb:/data/db
|
||||
command: mongod --logpath=/dev/null # -- quiet
|
||||
restart: always
|
||||
|
||||
ipfs-bch-wallet:
|
||||
ipfs-bch-wallet-service:
|
||||
build: .
|
||||
#image: christroutner/ipfs-service-provider:v5.0.1
|
||||
container_name: ipfs-service
|
||||
container_name: ipfs-bch-wallet-service
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
@@ -26,9 +26,12 @@ services:
|
||||
links:
|
||||
- mongo-bch-service
|
||||
ports:
|
||||
- '5010:5010' # <host port>:<container port>
|
||||
- '4001:4001'
|
||||
- '4003:4003'
|
||||
# <host port>:<container port>
|
||||
- '5032:5032' # REST API
|
||||
- '5668:5668' # TCP
|
||||
- '5669:5669' # Websockets
|
||||
- '5670:5670' # WebRTC
|
||||
|
||||
volumes:
|
||||
- ../data/ipfsdata:/home/safeuser/ipfs-bch-wallet-service/.ipfsdata
|
||||
- ./start-production.sh:/home/safeuser/ipfs-bch-wallet-service/start-production.sh
|
||||
|
||||
@@ -16,9 +16,6 @@ export COORD_NAME=bch-wallet-service-generic
|
||||
# and you must forward that subdomain to the IPFS_WS_PORT.
|
||||
#export CR_DOMAIN=subdomain.yourdomain.com
|
||||
|
||||
# Debug level. 0 = minimal info. 2 = max info.
|
||||
export DEBUG_LEVEL=2
|
||||
|
||||
# BCH Wallet Service Settings
|
||||
# Change APISERVER to point to your own installation of bch-api
|
||||
export APISERVER=https://api.fullstack.cash/v5/
|
||||
@@ -32,7 +29,7 @@ export FULLSTACKPASS=demo
|
||||
|
||||
|
||||
# Production database connection string.
|
||||
export DBURL=mongodb://172.17.0.1:5555/ipfs-service-prod
|
||||
export DBURL=mongodb://172.17.0.1:5557/ipfs-service-prod
|
||||
|
||||
# Configure REST API port
|
||||
export PORT=5010
|
||||
@@ -41,11 +38,17 @@ export PORT=5010
|
||||
export SVC_ENV=prod
|
||||
export IPFS_HOST=172.17.0.1
|
||||
export IPFS_API_PORT=5001
|
||||
export IPFS_TCP_PORT=4001
|
||||
export IPFS_WS_PORT=4003
|
||||
export IPFS_TCP_PORT=5668
|
||||
export IPFS_WS_PORT=5669
|
||||
export IPFS_WEB_RTC_PORT=5670
|
||||
|
||||
# Set the debug level for helia-coord. 0-3.
|
||||
# 0 = no debug logs. 3 = maximum debug logs.
|
||||
export DEBUG_LEVEL=0
|
||||
|
||||
# Use this if the IPFS node has a publically accessible IP address.
|
||||
#export CONNECT_PREF=direct
|
||||
# Use this if the IPFS node is behind a NAT or Firewall and cannot be accessed directly.
|
||||
export CONNECT_PREF=cr
|
||||
|
||||
npm start
|
||||
|
||||
+26
-58
@@ -51,70 +51,37 @@ class Admin {
|
||||
// used by the Listing Manager and test scripts, in order access private API
|
||||
// functions.
|
||||
async createSystemUser () {
|
||||
// Create the system user.
|
||||
try {
|
||||
context.password = _this._randomString(20)
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'system@system.com',
|
||||
password: context.password,
|
||||
name: 'admin'
|
||||
}
|
||||
}
|
||||
const context = {
|
||||
email: 'system@system.com',
|
||||
name: 'admin',
|
||||
password: _this.config.adminPassword || _this._randomString(20),
|
||||
type: 'admin'
|
||||
}
|
||||
const result = await _this.axios.request(options)
|
||||
// console.log('admin.data: ', result.data)
|
||||
// Check if the user already exists
|
||||
let adminUser = await _this.User.findOne({ email: context.email })
|
||||
|
||||
context.email = result.data.user.email
|
||||
context.id = result.data.user._id
|
||||
context.token = result.data.token
|
||||
if (adminUser) {
|
||||
// Update the password
|
||||
adminUser.password = context.password
|
||||
} else {
|
||||
// Create a new admin user
|
||||
adminUser = new _this.User(context)
|
||||
}
|
||||
// Update context with the new user id and token
|
||||
context.id = adminUser._id
|
||||
context.token = await adminUser.generateToken()
|
||||
|
||||
// Get the mongoDB entry
|
||||
const user = await _this.User.findById(context.id)
|
||||
// Save the user
|
||||
await adminUser.save()
|
||||
|
||||
// Change the user type to admin
|
||||
user.type = 'admin'
|
||||
// console.log(`user created: ${JSON.stringify(user, null, 2)}`)
|
||||
|
||||
// Save the user model.
|
||||
await user.save()
|
||||
|
||||
// console.log(`admin user created: ${JSON.stringify(result.body, null, 2)}`)
|
||||
// console.log(`with password: ${context.password}`)
|
||||
|
||||
// Write out the system user information to a JSON file that external
|
||||
// applications like the Task Manager and the test scripts can access.
|
||||
|
||||
await jsonFiles.writeJSON(context, JSON_PATH)
|
||||
// console.log('context: ', context)
|
||||
// console.log('JSON_PATH: ', JSON_PATH)
|
||||
// Write the user data to the JSON file
|
||||
await _this.jsonFiles.writeJSON(context, JSON_PATH)
|
||||
|
||||
return context
|
||||
} catch (err) {
|
||||
// Handle existing system user.
|
||||
if (err.response.status === 422) {
|
||||
try {
|
||||
// Delete the existing user
|
||||
await _this.deleteExistingSystemUser()
|
||||
|
||||
// Call this function again.
|
||||
return _this.createSystemUser()
|
||||
} catch (err2) {
|
||||
console.error(
|
||||
'Error in admin.js/createSystemUser() while trying generate new system user.'
|
||||
)
|
||||
// process.end(1)
|
||||
throw err2
|
||||
}
|
||||
} else {
|
||||
console.log('Error in admin.js/createSystemUser: ')
|
||||
// process.end(1)
|
||||
throw err
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error in admin.js/createSystemUser()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +96,7 @@ class Admin {
|
||||
})
|
||||
|
||||
await _this.User.deleteOne({ email: 'system@system.com' })
|
||||
return true
|
||||
} catch (err) {
|
||||
console.log('Error in admin.js/deleteExistingSystemUser()')
|
||||
throw err
|
||||
@@ -152,7 +120,7 @@ class Admin {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
data: {
|
||||
email: 'system@system.com',
|
||||
email: existingUser.email,
|
||||
password: existingUser.password
|
||||
}
|
||||
}
|
||||
|
||||
+20
-5
@@ -9,17 +9,15 @@ import BCHJS from '@psf/bch-js'
|
||||
|
||||
// Load individual adapter libraries.
|
||||
import IPFSAdapter from './ipfs/index.js'
|
||||
|
||||
import LocalDB from './localdb/index.js'
|
||||
import LogsAPI from './logapi.js'
|
||||
import Passport from './passport.js'
|
||||
import Nodemailer from './nodemailer.js'
|
||||
|
||||
// const { wlogger } = require('./wlogger')
|
||||
import JSONFiles from './json-files.js'
|
||||
|
||||
import FullStackJWT from './fullstack-jwt.js'
|
||||
import config from '../../config/index.js'
|
||||
import Wallet from './wallet.adapter.js'
|
||||
|
||||
class Adapters {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -31,7 +29,15 @@ class Adapters {
|
||||
this.passport = new Passport()
|
||||
this.nodemailer = new Nodemailer()
|
||||
this.jsonFiles = new JSONFiles()
|
||||
this.bchjs = new BCHJS({ restURL: this.config.apiServer })
|
||||
this.bchjs = new BCHJS({ restURL: config.apiServer })
|
||||
this.config = config
|
||||
|
||||
const walletConfig = {}
|
||||
if (config.walletInterface === 'web2') {
|
||||
walletConfig.restURL = config.apiServer
|
||||
walletConfig.interface = 'rest-api'
|
||||
}
|
||||
this.wallet = new Wallet(walletConfig)
|
||||
|
||||
// Get a valid JWT API key and instance bch-js.
|
||||
this.fullStackJwt = new FullStackJWT(config)
|
||||
@@ -39,10 +45,11 @@ class Adapters {
|
||||
|
||||
async start () {
|
||||
try {
|
||||
let apiToken
|
||||
if (this.config.getJwtAtStartup) {
|
||||
// Get a JWT token and instantiate bch-js with it. Then pass that instance
|
||||
// to all the rest of the apps controllers and adapters.
|
||||
await this.fullStackJwt.getJWT()
|
||||
apiToken = await this.fullStackJwt.getJWT()
|
||||
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
|
||||
this.bchjs = await this.fullStackJwt.instanceBchjs()
|
||||
} else {
|
||||
@@ -52,6 +59,14 @@ class Adapters {
|
||||
this.bchjs = new BCHJS({ restURL: this.config.apiServer })
|
||||
}
|
||||
|
||||
// Create a default instance of minimal-slp-wallet without initializing it
|
||||
// (without retrieving the wallets UTXOs). This instance will be overwritten
|
||||
// if the operator has configured BCH payments.
|
||||
console.log('\nCreating default startup wallet. This wallet may be overwritten.')
|
||||
await this.wallet.instanceWalletWithoutInitialization({}, { apiToken })
|
||||
this.bchjs = this.wallet.bchWallet.bchjs
|
||||
// console.log('adapters/index.js this.bchjs.restURL: ', this.bchjs.restURL)
|
||||
|
||||
// Start the IPFS node.
|
||||
// Do not start these adapters if this is an e2e test.
|
||||
if (this.config.env !== 'test') {
|
||||
|
||||
+17
-205
@@ -3,33 +3,12 @@
|
||||
This library deals with IPFS so that the apps business logic doesn't need
|
||||
to have any specific knowledge of the js-ipfs library.
|
||||
|
||||
TODO: Add the external IP address to the list of multiaddrs advertised by
|
||||
this node. See this GitHub Issue for details:
|
||||
https://github.com/Permissionless-Software-Foundation/ipfs-service-provider/issues/38
|
||||
Node creation is delegated to helia-coord's CreateHeliaNode factory,
|
||||
which encapsulates all helia/libp2p dependencies and configuration.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import { createHelia } from 'helia'
|
||||
import fs from 'fs'
|
||||
import { FsBlockstore } from 'blockstore-fs'
|
||||
import { FsDatastore } from 'datastore-fs'
|
||||
import { createLibp2p } from 'libp2p'
|
||||
import { tcp } from '@libp2p/tcp'
|
||||
import { noise } from '@chainsafe/libp2p-noise'
|
||||
import { yamux } from '@chainsafe/libp2p-yamux'
|
||||
// import { bootstrap } from '@libp2p/bootstrap'
|
||||
// import { identifyService } from 'libp2p/identify'
|
||||
import { identify } from '@libp2p/identify'
|
||||
// import { circuitRelayServer, circuitRelayTransport } from 'libp2p/circuit-relay'
|
||||
import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||||
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
|
||||
import { webSockets } from '@libp2p/websockets'
|
||||
import { publicIpv4 } from 'public-ip'
|
||||
import { multiaddr } from '@multiformats/multiaddr'
|
||||
import { webRTC } from '@libp2p/webrtc'
|
||||
import { keychain } from '@libp2p/keychain'
|
||||
import { defaultLogger } from '@libp2p/logger'
|
||||
import { unixfs } from '@helia/unixfs'
|
||||
import CreateHeliaNode from 'helia-coord/create-helia-node'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../config/index.js'
|
||||
@@ -39,59 +18,44 @@ import JsonFiles from '../json-files.js'
|
||||
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
||||
import * as url from 'url'
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
// console.log('__dirname: ', __dirname)
|
||||
|
||||
const ROOT_DIR = `${__dirname}../../../`
|
||||
const IPFS_DIR = `${__dirname}../../../.ipfsdata/ipfs`
|
||||
|
||||
class IpfsAdapter {
|
||||
constructor (localConfig) {
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
this.fs = fs
|
||||
this.createLibp2p = createLibp2p
|
||||
this.publicIp = publicIpv4
|
||||
this.multiaddr = multiaddr
|
||||
this.jsonFiles = new JsonFiles()
|
||||
this.keychain = keychain
|
||||
this.createHelia = createHelia
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.start = this.start.bind(this)
|
||||
this.createNode = this.createNode.bind(this)
|
||||
this.stop = this.stop.bind(this)
|
||||
this.ensureBlocksDir = this.ensureBlocksDir.bind(this)
|
||||
this.getSeed = this.getSeed.bind(this)
|
||||
this.getKeychain = this.getKeychain.bind(this)
|
||||
}
|
||||
|
||||
// Start an IPFS node.
|
||||
async start () {
|
||||
try {
|
||||
// Ensure the directory structure exists that is needed by the IPFS node to store data.
|
||||
this.ensureBlocksDir()
|
||||
// Create the helia-coord node factory with production configuration.
|
||||
this.heliaNode = new CreateHeliaNode({
|
||||
ipfsDir: IPFS_DIR,
|
||||
tcpPort: this.config.ipfsTcpPort,
|
||||
wsPort: this.config.ipfsWsPort,
|
||||
isCircuitRelay: this.config.isCircuitRelay,
|
||||
getSeed: this.getSeed
|
||||
})
|
||||
|
||||
// Create an IPFS node
|
||||
const ipfs = await this.createNode()
|
||||
// console.log('ipfs: ', ipfs)
|
||||
// Start the IPFS node (creates dirs, libp2p, helia, detects public IP)
|
||||
const ipfs = await this.heliaNode.start()
|
||||
|
||||
this.id = this.heliaNode.id
|
||||
this.multiaddrs = this.heliaNode.multiaddrs
|
||||
|
||||
this.id = ipfs.libp2p.peerId.toString()
|
||||
console.log('IPFS ID: ', this.id)
|
||||
|
||||
// Attempt to guess our ip4 IP address.
|
||||
const ip4 = await this.publicIp()
|
||||
let detectedMultiaddr = `/ip4/${ip4}/tcp/${this.config.ipfsTcpPort}/p2p/${this.id}`
|
||||
detectedMultiaddr = this.multiaddr(detectedMultiaddr)
|
||||
|
||||
// Get the multiaddrs for the node.
|
||||
const multiaddrs = ipfs.libp2p.getMultiaddrs()
|
||||
multiaddrs.push(detectedMultiaddr)
|
||||
console.log('Multiaddrs: ', multiaddrs)
|
||||
|
||||
this.multiaddrs = multiaddrs
|
||||
console.log('Multiaddrs: ', this.multiaddrs)
|
||||
|
||||
// Signal that this adapter is ready.
|
||||
this.isReady = true
|
||||
@@ -101,137 +65,6 @@ class IpfsAdapter {
|
||||
return this.ipfs
|
||||
} catch (err) {
|
||||
console.error('Error in ipfs.js/start()')
|
||||
|
||||
// If IPFS crashes because the /blocks directory is full, wipe the directory.
|
||||
// if (err.message.includes('No space left on device')) {
|
||||
// this.rmBlocksDir()
|
||||
// }
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getKeychain (datastore) {
|
||||
const keychainInit = {
|
||||
pass: await this.getSeed()
|
||||
}
|
||||
|
||||
const chain = this.keychain(keychainInit)({
|
||||
datastore,
|
||||
logger: defaultLogger()
|
||||
})
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
// This function creates an IPFS node using Helia.
|
||||
// It returns the node as an object.
|
||||
async createNode () {
|
||||
try {
|
||||
// Create block and data stores.
|
||||
const blockstore = new FsBlockstore(`${IPFS_DIR}/blockstore`)
|
||||
const datastore = new FsDatastore(`${IPFS_DIR}/datastore`)
|
||||
|
||||
// const keychainInit = {
|
||||
// pass: await this.getSeed()
|
||||
// }
|
||||
|
||||
// Create an identity
|
||||
let peerId
|
||||
// console.log('this.keychain: ', this.keychain)
|
||||
// const chain = this.keychain(keychainInit)({
|
||||
// datastore,
|
||||
// logger: defaultLogger()
|
||||
// })
|
||||
const chain = await this.getKeychain(datastore)
|
||||
try {
|
||||
peerId = await chain.exportPeerId('myKey')
|
||||
} catch (err) {
|
||||
await chain.createKey('myKey', 'Ed25519', 4096)
|
||||
peerId = await chain.exportPeerId('myKey')
|
||||
}
|
||||
|
||||
// Configure services
|
||||
const services = {
|
||||
identify: identify(),
|
||||
pubsub: gossipsub({ allowPublishToZeroPeers: true })
|
||||
}
|
||||
if (this.config.isCircuitRelay) {
|
||||
console.log('Helia (IPFS) node IS configured as Circuit Relay')
|
||||
services.relay = circuitRelayServer({ // makes the node function as a relay server
|
||||
hopTimeout: 30 * 1000, // incoming relay requests must be resolved within this time limit
|
||||
advertise: true,
|
||||
reservations: {
|
||||
maxReservations: 15, // how many peers are allowed to reserve relay slots on this server
|
||||
reservationClearInterval: 300 * 1000, // how often to reclaim stale reservations
|
||||
applyDefaultLimit: true, // whether to apply default data/duration limits to each relayed connection
|
||||
defaultDurationLimit: 2 * 60 * 1000, // the default maximum amount of time a relayed connection can be open for
|
||||
defaultDataLimit: BigInt(2 << 7), // the default maximum number of bytes that can be transferred over a relayed connection
|
||||
maxInboundHopStreams: 32, // how many inbound HOP streams are allow simultaneously
|
||||
maxOutboundHopStreams: 64 // how many outbound HOP streams are allow simultaneously
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log('Helia (IPFS) node IS NOT configured as Circuit Relay')
|
||||
}
|
||||
|
||||
// Configure transports
|
||||
let transports
|
||||
if (process.env.CONNECT_PREF === 'direct') {
|
||||
transports = [
|
||||
tcp(),
|
||||
webSockets(),
|
||||
webRTC()
|
||||
]
|
||||
} else {
|
||||
transports = [
|
||||
tcp(),
|
||||
webSockets(),
|
||||
circuitRelayTransport({
|
||||
discoverRelays: 3,
|
||||
reservationConcurrency: 3
|
||||
}),
|
||||
webRTC()
|
||||
]
|
||||
}
|
||||
|
||||
// libp2p is the networking layer that underpins Helia
|
||||
const libp2p = await this.createLibp2p({
|
||||
peerId,
|
||||
datastore,
|
||||
addresses: {
|
||||
listen: [
|
||||
'/ip4/127.0.0.1/tcp/0',
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`,
|
||||
'/webrtc'
|
||||
]
|
||||
},
|
||||
transports,
|
||||
connectionEncryption: [
|
||||
noise()
|
||||
],
|
||||
streamMuxers: [
|
||||
yamux()
|
||||
],
|
||||
services
|
||||
})
|
||||
|
||||
// create a Helia node
|
||||
const helia = await this.createHelia({
|
||||
blockstore,
|
||||
datastore,
|
||||
libp2p
|
||||
})
|
||||
|
||||
// Attach IPFS file system.
|
||||
const fs = unixfs(helia)
|
||||
helia.fs = fs
|
||||
|
||||
return helia
|
||||
} catch (err) {
|
||||
console.error('Error creating Helia node: ', err)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -242,25 +75,6 @@ class IpfsAdapter {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ensure that the directories exist to store blocks from the IPFS network.
|
||||
// This function is called at startup, before the IPFS node is started.
|
||||
ensureBlocksDir () {
|
||||
try {
|
||||
!this.fs.existsSync(`${ROOT_DIR}.ipfsdata`) && this.fs.mkdirSync(`${ROOT_DIR}.ipfsdata`)
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}`) && this.fs.mkdirSync(`${IPFS_DIR}`)
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}/blockstore`) && this.fs.mkdirSync(`${IPFS_DIR}/blockstore`)
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`)
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in adapters/ipfs.js/ensureBlocksDir(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This function opens the seed used to generate the key for this IPFS peer.
|
||||
// The seed is stored in a JSON file. If it doesn't exist, a new one is created.
|
||||
async getSeed () {
|
||||
@@ -280,8 +94,6 @@ class IpfsAdapter {
|
||||
await this.jsonFiles.writeJSON(seed, filename)
|
||||
}
|
||||
|
||||
// console.log('getSeed() seed: ', seed)
|
||||
|
||||
return seed
|
||||
} catch (err) {
|
||||
console.error('Error in adapters/ipfs/ipfs.js/getSeed()')
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
// Load Mongoose models.
|
||||
import Users from './models/users.js'
|
||||
import Usage from './models/usage.js'
|
||||
|
||||
class LocalDB {
|
||||
constructor () {
|
||||
// Encapsulate dependencies
|
||||
this.Users = Users
|
||||
this.Usage = Usage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
Usage data model
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
const Usage = new mongoose.Schema({
|
||||
ip: { type: String },
|
||||
url: { type: String },
|
||||
method: { type: String },
|
||||
timestamp: { type: Date }
|
||||
})
|
||||
|
||||
export default mongoose.model('usage', Usage)
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
Wallet adapter. This library instantiates minimal-slp-wallet based on different
|
||||
configuration settings. That single instance of minimal-slp-wallet is then
|
||||
easily passed around the rest of this application.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../config/index.js'
|
||||
import JsonFiles from './json-files.js'
|
||||
|
||||
class Wallet {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
this.BchWallet = BchWallet
|
||||
this.jsonFiles = new JsonFiles()
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.instanceWalletWithoutInitialization = this.instanceWalletWithoutInitialization.bind(this)
|
||||
this._instanceWallet = this._instanceWallet.bind(this)
|
||||
this.openWallet = this.openWallet.bind(this)
|
||||
this.instanceWallet = this.instanceWallet.bind(this)
|
||||
this.incrementNextAddress = this.incrementNextAddress.bind(this)
|
||||
this.getKeyPair = this.getKeyPair.bind(this)
|
||||
this.optimize = this.optimize.bind(this)
|
||||
this.getBalance = this.getBalance.bind(this)
|
||||
}
|
||||
|
||||
// This is used for initializing the wallet, without waiting to update the wallet
|
||||
// UTXOs from the blockchain.
|
||||
// This is useful when the wallet is simply needed to make calls to the blockchain,
|
||||
// and there is no need to hydrate it with UTXO data.
|
||||
async instanceWalletWithoutInitialization (walletData = {}, advancedConfig = {}) {
|
||||
try {
|
||||
// Use the apiToken from the config settings, if one is not passed-in at
|
||||
// run-time.
|
||||
if (!advancedConfig.apiToken) {
|
||||
advancedConfig.apiToken = this.config.apiToken
|
||||
}
|
||||
|
||||
// If an authentication password is passed in the config, add it.
|
||||
if (this.config.authPass) {
|
||||
advancedConfig.authPass = this.config.authPass
|
||||
}
|
||||
|
||||
// Detect and configure different blockchain infrastructure settings.
|
||||
if (this.config.walletInterface === 'web2') {
|
||||
advancedConfig.interface = 'rest-api'
|
||||
advancedConfig.restURL = this.config.apiServer
|
||||
} else {
|
||||
// By default use the web3 Cash Stack (https://cashstack.info)
|
||||
advancedConfig.interface = 'consumer-api'
|
||||
advancedConfig.restURL = this.config.apiServer
|
||||
}
|
||||
|
||||
// console.log('advancedConfig setting when creating wallet: ', advancedConfig)
|
||||
|
||||
// Instantiate minimal-slp-wallet.
|
||||
if (walletData.mnemonic) {
|
||||
// Instance the wallet using the mnemonic passed in to this function.
|
||||
console.log('Mnemonic provided to wallet library from calling function.')
|
||||
this.bchWallet = await this._instanceWallet(walletData.mnemonic, advancedConfig)
|
||||
} else if (this.config.mnemonic) {
|
||||
// Otherwise use the mnemonic in the config setting or passed as an environment variable.
|
||||
console.log('Mnemonic retrieved from config file or environment variable.')
|
||||
this.bchWallet = await this._instanceWallet(this.config.mnemonic, advancedConfig)
|
||||
} else {
|
||||
// If no mnemonic is provided, then generate a new mnemonic to create the wallet.
|
||||
console.log('New mnemonic generated.')
|
||||
this.bchWallet = await this._instanceWallet(undefined, advancedConfig)
|
||||
}
|
||||
|
||||
// Wait for wallet to initialize.
|
||||
await this.bchWallet.walletInfoPromise
|
||||
console.log('BCH wallet initialized.')
|
||||
console.log(`Wallet address: ${this.bchWallet.walletInfo.cashAddress}`)
|
||||
console.log(`Wallet mnemonic: ${this.bchWallet.walletInfo.mnemonic}`)
|
||||
console.log(`Wallet backend: ${this.bchWallet.bchjs.restURL}`)
|
||||
|
||||
return this.bchWallet
|
||||
} catch (err) {
|
||||
console.error('Error in instanceWalletWithoutInitialization()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used for easier mocking of unit tests.
|
||||
async _instanceWallet (mnemonic, config) {
|
||||
const wallet = new this.BchWallet(mnemonic, config)
|
||||
await wallet.walletInfoPromise
|
||||
|
||||
return wallet
|
||||
}
|
||||
|
||||
// Open the wallet file, or create one if the file doesn't exist.
|
||||
// Does not instance the wallet. The output of this function is expected to
|
||||
// be passed to instanceWallet().
|
||||
async openWallet (advancedConfig = {}) {
|
||||
try {
|
||||
let walletData
|
||||
|
||||
// Try to open the wallet.json file.
|
||||
try {
|
||||
walletData = await this.jsonFiles.readJSON(this.config.walletFile)
|
||||
walletData = walletData.wallet
|
||||
} catch (err) {
|
||||
// Create a new wallet file if one does not already exist.
|
||||
console.log('Wallet file not found. Creating new wallet.json file.')
|
||||
|
||||
// Create a new wallet.
|
||||
// No-Update flag creates wallet without making any network calls.
|
||||
const walletInstance = await this.instanceWalletWithoutInitialization({}, advancedConfig)
|
||||
|
||||
walletData = walletInstance.walletInfo
|
||||
|
||||
// Add the nextAddress property
|
||||
walletData.nextAddress = 1
|
||||
|
||||
// Write the wallet data to the JSON file.
|
||||
await this.jsonFiles.writeJSON({ wallet: walletData }, this.config.walletFile)
|
||||
}
|
||||
|
||||
// console.log('walletData: ', walletData)
|
||||
return walletData
|
||||
} catch (err) {
|
||||
console.error('Error in openWallet()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Create an instance of minimal-slp-wallet. Same as
|
||||
// instanceWalletWithoutInitialization(), but waits for the wallet to initialize
|
||||
// its UTXOs (wallet balance and tokens).
|
||||
async instanceWallet (walletData = {}, advancedConfig = {}) {
|
||||
try {
|
||||
// Instance the wallet without initialization.
|
||||
await this.instanceWalletWithoutInitialization(walletData, advancedConfig)
|
||||
|
||||
// Initialize the wallet
|
||||
await this.bchWallet.initialize()
|
||||
|
||||
return this.bchWallet
|
||||
} catch (err) {
|
||||
console.error('Error in wallet.js/instanceWallet()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Increments the 'nextAddress' property in the wallet file. This property
|
||||
// indicates the HD index that should be used to generate a key pair for
|
||||
// storing funds for Offers.
|
||||
// This function opens the wallet file, increments the nextAddress property,
|
||||
// then saves the change to the wallet file.
|
||||
async incrementNextAddress () {
|
||||
try {
|
||||
const walletData = await this.openWallet()
|
||||
|
||||
await this.instanceWalletWithoutInitialization(walletData)
|
||||
|
||||
walletData.nextAddress++
|
||||
// console.log('walletData finish: ', walletData)
|
||||
|
||||
await this.jsonFiles.writeJSON({ wallet: walletData }, this.config.walletFile)
|
||||
|
||||
// Update the working instance of the wallet.
|
||||
this.bchWallet.walletInfo.nextAddress++
|
||||
// console.log('this.bchWallet.walletInfo: ', this.bchWallet.walletInfo)
|
||||
|
||||
return walletData.nextAddress
|
||||
} catch (err) {
|
||||
console.error('Error in incrementNextAddress()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This method returns an object that contains a private key WIF, public address,
|
||||
// and the index of the HD wallet that the key pair was generated from.
|
||||
// TODO: Allow input integer. If input is used, use that as the index. If no
|
||||
// input is provided, then call incrementNextAddress().
|
||||
async getKeyPair (hdIndex = 0) {
|
||||
try {
|
||||
if (!hdIndex) {
|
||||
// Increment the HD index and generate a new key pair.
|
||||
hdIndex = await this.incrementNextAddress()
|
||||
}
|
||||
|
||||
const mnemonic = this.bchWallet.walletInfo.mnemonic
|
||||
|
||||
// root seed buffer
|
||||
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
|
||||
const masterHDNode = this.bchWallet.bchjs.HDNode.fromSeed(rootSeed)
|
||||
|
||||
// HDNode of BIP44 account
|
||||
// const account = this.bchWallet.bchjs.HDNode.derivePath(masterHDNode, "m/44'/245'/0'")
|
||||
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||
const cashAddress = this.bchWallet.bchjs.HDNode.toCashAddress(childNode)
|
||||
console.log('Generating a new key pair for cashAddress: ', cashAddress)
|
||||
|
||||
const wif = this.bchWallet.bchjs.HDNode.toWIF(childNode)
|
||||
|
||||
const outObj = {
|
||||
cashAddress,
|
||||
wif,
|
||||
hdIndex
|
||||
}
|
||||
return outObj
|
||||
} catch (err) {
|
||||
console.error('Error in getKeyPair()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize the wallet by consolidating the UTXOs.
|
||||
async optimize () {
|
||||
const UTXO_THREASHOLD = 7
|
||||
// Do a dry-run first to see if there are enough UTXOs worth consolidating.
|
||||
const dryRunOut = await this.bchWallet.optimize(true)
|
||||
if (dryRunOut.bchUtxoCnt > UTXO_THREASHOLD) {
|
||||
// Consolidate BCH UTXOs if the count is above the threashold.
|
||||
const txids = await this.bchWallet.optimize()
|
||||
console.log(`Wallet optimized with these return values: ${JSON.stringify(txids, null, 2)}`)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Get the balance of the wallet in sats and PSF tokens.
|
||||
// This function is called by the GET /entry/balance controller.
|
||||
async getBalance () {
|
||||
const balance = await this.bchWallet.getBalance()
|
||||
// console.log('balance: ', balance)
|
||||
const tokens = await this.bchWallet.listTokens()
|
||||
// console.log('tokens: ', tokens)
|
||||
// Find the array entry for the PSF token
|
||||
const psfTokens = tokens.find(x => x.tokenId === '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0')
|
||||
// console.log('psfTokens: ', psfTokens)
|
||||
let psfBalance = 0
|
||||
if (psfTokens) {
|
||||
psfBalance = psfTokens.qty
|
||||
}
|
||||
const outObj = {
|
||||
satBalance: balance,
|
||||
psfBalance,
|
||||
success: true
|
||||
}
|
||||
return outObj
|
||||
}
|
||||
}
|
||||
|
||||
export default Wallet
|
||||
@@ -17,10 +17,17 @@ import config from '../../config/index.js'
|
||||
class Controllers {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.adapters = new Adapters()
|
||||
this.adapters = new Adapters(localConfig)
|
||||
this.useCases = new UseCases({ adapters: this.adapters })
|
||||
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
|
||||
this.config = config
|
||||
|
||||
// Bind 'this' object to all subfunction
|
||||
this.initAdapters = this.initAdapters.bind(this)
|
||||
this.initUseCases = this.initUseCases.bind(this)
|
||||
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
||||
this.attachControllers = this.attachControllers.bind(this)
|
||||
this.attachRPCControllers = this.attachRPCControllers.bind(this)
|
||||
}
|
||||
|
||||
// Spin up any adapter libraries that have async startup needs.
|
||||
|
||||
@@ -13,6 +13,8 @@ import ContactRESTController from './contact/index.js'
|
||||
import LogsRESTController from './logs/index.js'
|
||||
import IpfsRESTController from './ipfs/index.js'
|
||||
import BCHRESTController from './bch/index.js'
|
||||
import config from '../../../config/index.js'
|
||||
import UsageRESTController from './usage/index.js'
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -30,7 +32,11 @@ class RESTControllers {
|
||||
)
|
||||
}
|
||||
|
||||
// console.log('Controllers localConfig: ', localConfig)
|
||||
// Bind 'this' object to all subfunctions.
|
||||
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
}
|
||||
|
||||
attachRESTControllers (app) {
|
||||
@@ -39,13 +45,15 @@ class RESTControllers {
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Attach the REST API Controllers associated with the /auth route
|
||||
const authRESTController = new AuthRESTController(dependencies)
|
||||
authRESTController.attach(app)
|
||||
if (!this.config.noMongo) {
|
||||
// Attach the REST API Controllers associated with the /auth route
|
||||
const authRESTController = new AuthRESTController(dependencies)
|
||||
authRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /user route
|
||||
const userRouter = new UserRouter(dependencies)
|
||||
userRouter.attach(app)
|
||||
// Attach the REST API Controllers associated with the /user route
|
||||
const userRouter = new UserRouter(dependencies)
|
||||
userRouter.attach(app)
|
||||
}
|
||||
|
||||
// Attach the REST API Controllers associated with the /contact route
|
||||
const contactRESTController = new ContactRESTController(dependencies)
|
||||
@@ -62,6 +70,10 @@ class RESTControllers {
|
||||
// Attach the REST API Controllers associated with the /fulcrum route
|
||||
const bchRESTController = new BCHRESTController(dependencies)
|
||||
bchRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /usage route
|
||||
const usageRESTController = new UsageRESTController(dependencies)
|
||||
usageRESTController.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,26 @@ class IpfsRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
// Return information on IPFS peers this node is connected to.
|
||||
/**
|
||||
* @api {post} /ipfs/peers Get information on IPFS peers this node is connected to
|
||||
* @apiPermission public
|
||||
* @apiName GetIpfsPeers
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiParam {Boolean} [showAll=false] Whether to include detailed peer data
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/peers \
|
||||
* -d '{"showAll": false}'
|
||||
*
|
||||
* @apiSuccess {Object[]} peers Array of peer objects
|
||||
* @apiSuccess {String} peers[].peer Peer ID
|
||||
* @apiSuccess {String} peers[].name Peer name
|
||||
* @apiSuccess {String} peers[].protocol Protocol used by the peer
|
||||
* @apiSuccess {String} peers[].version Peer version
|
||||
* @apiSuccess {String} peers[].connectionAddr Connection address
|
||||
* @apiSuccess {Object} [peers[].peerData] Detailed peer data (when showAll=true)
|
||||
*/
|
||||
async getPeers (ctx) {
|
||||
try {
|
||||
const showAll = ctx.request.body.showAll
|
||||
@@ -73,6 +92,24 @@ class IpfsRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /ipfs/relays Get data about the known Circuit Relays
|
||||
* @apiPermission public
|
||||
* @apiName GetIpfsRelays
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiDescription Returns information about Circuit Relays, both v1 and v2, that this node knows about. V2 relays are hydrated with peer data from the connected peers list.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/relays
|
||||
*
|
||||
* @apiSuccess {Object} relays Object containing relay information
|
||||
* @apiSuccess {Object[]} relays.v2Relays Array of v2 Circuit Relay objects
|
||||
* @apiSuccess {String} relays.v2Relays[].ipfsId IPFS ID of the relay
|
||||
* @apiSuccess {String} relays.v2Relays[].name Name of the relay (hydrated from peer data)
|
||||
* @apiSuccess {String} relays.v2Relays[].description Description of the relay (hydrated from peer data)
|
||||
* @apiSuccess {Object[]} relays.v1Relays Array of v1 Circuit Relay configurations
|
||||
*/
|
||||
// Get data about the known Circuit Relays. Hydrate with data from peers list.
|
||||
async getRelays (ctx) {
|
||||
try {
|
||||
@@ -86,6 +123,24 @@ class IpfsRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /ipfs/connect Connect to a specific IPFS peer
|
||||
* @apiPermission public
|
||||
* @apiName ConnectToIpfsPeer
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiDescription Attempts to establish a connection to a specific IPFS peer using the provided multiaddr. Optionally returns detailed information about the connection.
|
||||
*
|
||||
* @apiParam {String} multiaddr Multiaddress of the peer to connect to (required)
|
||||
* @apiParam {Boolean} [getDetails=false] Whether to return detailed connection information
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST localhost:5001/ipfs/connect \
|
||||
* -d '{"multiaddr": "/ip4/161.35.99.207/tcp/4001/p2p/12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu", "getDetails": false}'
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates whether the connection attempt was successful
|
||||
* @apiSuccess {Object} [details] Additional connection details (when getDetails=true)
|
||||
*/
|
||||
async connect (ctx) {
|
||||
try {
|
||||
const multiaddr = ctx.request.body.multiaddr
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
REST API Controller library for the /usage route
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
|
||||
// Local libraries
|
||||
import wlogger from '../../../adapters/wlogger.js'
|
||||
|
||||
class UsageRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.getStatus = this.getStatus.bind(this)
|
||||
this.getTopIps = this.getTopIps.bind(this)
|
||||
this.getTopEndpoints = this.getTopEndpoints.bind(this)
|
||||
this.handleError = this.handleError.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage Get status on IPFS infrastructure
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageStatus
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage
|
||||
*
|
||||
*/
|
||||
getStatus (ctx) {
|
||||
try {
|
||||
// const status = await this.adapters.ipfs.getStatus()
|
||||
const status = this.useCases.usage.getRestSummary()
|
||||
|
||||
ctx.body = { status }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getStatus(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage/ips Get top IP addresses consuming the REST API
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageIPs
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/ips
|
||||
*
|
||||
*/
|
||||
getTopIps (ctx) {
|
||||
try {
|
||||
const ips = this.useCases.usage.getTopIps()
|
||||
|
||||
ctx.body = { ips }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getTopIps(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage/endpoints Get top endpoints consumed from the REST API
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageEndpoints
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/endpoints
|
||||
*
|
||||
*/
|
||||
getTopEndpoints (ctx) {
|
||||
try {
|
||||
const endpoints = this.useCases.usage.getTopEndpoints()
|
||||
|
||||
ctx.body = { endpoints }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getTopEndpoints(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = IpfsRESTControllerLib
|
||||
export default UsageRESTControllerLib
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
REST API library for the /usage route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
import Router from 'koa-router'
|
||||
|
||||
// Local libraries.
|
||||
import UsageRESTControllerLib from './controller.js'
|
||||
import Validators from '../middleware/validators.js'
|
||||
|
||||
// let _this
|
||||
|
||||
class UsageRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.usageRESTController = new UsageRESTControllerLib(dependencies)
|
||||
this.validators = new Validators()
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/usage'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
// _this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.get('/', this.usageRESTController.getStatus)
|
||||
this.router.get('/ips', this.usageRESTController.getTopIps)
|
||||
this.router.get('/endpoints', this.usageRESTController.getTopEndpoints)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchRouter
|
||||
export default UsageRouter
|
||||
@@ -10,6 +10,8 @@ import UserRESTControllerLib from './controller.js'
|
||||
|
||||
import Validators from '../middleware/validators.js'
|
||||
|
||||
import config from '../../../../config/index.js'
|
||||
|
||||
let _this
|
||||
|
||||
class UserRouter {
|
||||
@@ -34,6 +36,7 @@ class UserRouter {
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.config = config
|
||||
this.userRESTController = new UserRESTControllerLib(dependencies)
|
||||
this.validators = new Validators()
|
||||
|
||||
@@ -52,7 +55,7 @@ class UserRouter {
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', this.userRESTController.createUser)
|
||||
this.router.post('/', this.createUser)
|
||||
this.router.get('/', this.getAll)
|
||||
this.router.get('/:id', this.getById)
|
||||
this.router.put('/:id', this.updateUser)
|
||||
@@ -63,6 +66,14 @@ class UserRouter {
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
|
||||
async createUser (ctx, next) {
|
||||
if (process.env.DISABLE_NEW_ACCOUNTS) {
|
||||
await _this.validators.ensureAdmin(ctx, next)
|
||||
}
|
||||
await _this.userRESTController.createUser(ctx, next)
|
||||
return true
|
||||
}
|
||||
|
||||
async getAll (ctx, next) {
|
||||
await _this.validators.ensureUser(ctx, next)
|
||||
await _this.userRESTController.getUsers(ctx, next)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
This Controller library is concerned with timer-based functions that are
|
||||
kicked off periodicially.
|
||||
kicked off periodically.
|
||||
*/
|
||||
|
||||
import config from '../../config/index.js'
|
||||
@@ -23,38 +23,76 @@ class TimerControllers {
|
||||
|
||||
this.debugLevel = localConfig.debugLevel
|
||||
|
||||
// Constants
|
||||
this.cleanUsageInterval = 60000 * 60 // 1 hour
|
||||
this.backupUsageInterval = 60000 * 10 // 10 minutes
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
|
||||
// Bind 'this' object to all subfunctions.
|
||||
this.exampleTimerFunc = this.exampleTimerFunc.bind(this)
|
||||
|
||||
// this.startTimers()
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
this.backupUsage = this.backupUsage.bind(this)
|
||||
}
|
||||
|
||||
// Start all the time-based controllers.
|
||||
startTimers () {
|
||||
// Any new timer control functions can be added here. They will be started
|
||||
// when the server starts.
|
||||
this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 10)
|
||||
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
|
||||
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
stopTimers () {
|
||||
clearInterval(this.optimizeWalletHandle)
|
||||
clearInterval(this.cleanUsageHandle)
|
||||
clearInterval(this.backupUsageHandle)
|
||||
}
|
||||
|
||||
// Replace this example function with your own timer handler.
|
||||
exampleTimerFunc (negativeTest) {
|
||||
// Clean the usage state so that stats reflect the last 24 hours.
|
||||
cleanUsage () {
|
||||
try {
|
||||
console.log('Example timer controller executed.')
|
||||
clearInterval(this.cleanUsageHandle)
|
||||
|
||||
if (negativeTest) throw new Error('test error')
|
||||
const now = new Date()
|
||||
console.log(`cleanUsage() Timer Controller executing at ${now.toLocaleString()}`)
|
||||
|
||||
this.useCases.usage.cleanUsage()
|
||||
|
||||
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in exampleTimerFunc(): ', err)
|
||||
console.error('Error in time-controller.js/cleanUsage(): ', err)
|
||||
|
||||
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
|
||||
|
||||
// Note: Do not throw an error. This is a top-level function.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Backup the usage stats to the database
|
||||
async backupUsage () {
|
||||
try {
|
||||
clearInterval(this.backupUsageHandle)
|
||||
|
||||
console.log('backupUsage() Timer Controller executing at ', new Date().toLocaleString())
|
||||
|
||||
// Clear the database of old usage data.
|
||||
await this.useCases.usage.clearUsage()
|
||||
|
||||
// Save the current usage snapshot to the database.
|
||||
await this.useCases.usage.saveUsage()
|
||||
|
||||
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in time-controller.js/backupUsage(): ', err)
|
||||
|
||||
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
|
||||
|
||||
// Note: Do not throw an error. This is a top-level function.
|
||||
return false
|
||||
|
||||
@@ -14,6 +14,7 @@ class BCHUseCases {
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.bchjs = this.adapters.bchjs
|
||||
// console.log('use-cases/bch/index.js restURL: ', this.bchjs.restURL)
|
||||
}
|
||||
|
||||
// Get transaction history for an address, sorted by block height.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// Local libraries
|
||||
import UserUseCases from './user.js'
|
||||
import BCHUseCases from './bch/index.js'
|
||||
import { UsageUseCases } from './usage-use-cases.js'
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -20,10 +21,14 @@ class UseCases {
|
||||
// console.log('use-cases/index.js localConfig: ', localConfig)
|
||||
this.user = new UserUseCases(localConfig)
|
||||
this.bch = new BCHUseCases(localConfig)
|
||||
this.usage = new UsageUseCases(localConfig)
|
||||
}
|
||||
|
||||
// Run any startup Use Cases at the start of the app.
|
||||
async start () {
|
||||
// Load the usage stats from the database
|
||||
await this.usage.loadUsage()
|
||||
|
||||
console.log('Async Use Cases have been started.')
|
||||
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
Use Case library for tracking usage. This library contains business logic
|
||||
for tracking the usage of REST API and JSON RPC calls. This library is used
|
||||
by admins to keep an eye on how many API calls were made in a 24-hour and
|
||||
1-hour time period.
|
||||
|
||||
Usage stats are held in memory. But they are periodically backed up to the
|
||||
Mongo database. On startup, the usage stats are loaded from the database.
|
||||
This allows the usage stats to be persisted across restarts.
|
||||
*/
|
||||
|
||||
// This global variable is used to share data between the REST middleware and
|
||||
// the Usage Use Case class instance.
|
||||
let restCalls = []
|
||||
|
||||
class UsageUseCases {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating Usage Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.UsageModel = this.adapters.localdb.Usage
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
this.getRestSummary = this.getRestSummary.bind(this)
|
||||
this.getTopIps = this.getTopIps.bind(this)
|
||||
this.getTopEndpoints = this.getTopEndpoints.bind(this)
|
||||
this.clearUsage = this.clearUsage.bind(this)
|
||||
this.saveUsage = this.saveUsage.bind(this)
|
||||
this.loadUsage = this.loadUsage.bind(this)
|
||||
|
||||
// State
|
||||
}
|
||||
|
||||
// Clean up the state by removing entries that are older than 24 hours. This
|
||||
// ensures stats reflect only the last 24 hours.
|
||||
// This function is called by a Timer Controller.
|
||||
cleanUsage () {
|
||||
try {
|
||||
const now = new Date()
|
||||
const twentyFourHoursAgo = now.getTime() - (60000 * 60 * 24)
|
||||
|
||||
console.log('cleanUsage() now: ', now)
|
||||
console.log('cleanUsage() restCalls.length before filtering: ', restCalls.length)
|
||||
restCalls = restCalls.filter(x => x.timestamp > twentyFourHoursAgo)
|
||||
console.log('cleanUsage() restCalls.length after filtering: ', restCalls.length)
|
||||
|
||||
return restCalls
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/cleanUsage()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Track the calls to a REST API
|
||||
getRestSummary (inObj = {}) {
|
||||
try {
|
||||
console.log(`getRestSummary(): There have been ${restCalls.length} REST calls`)
|
||||
|
||||
return restCalls.length
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getRestSummary()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the top 20 IP addresses from the stats.
|
||||
getTopIps () {
|
||||
try {
|
||||
const ips = restCalls.map(x => x.ip)
|
||||
// Create a Map to count occurrences of each IP address string
|
||||
const countMap = new Map()
|
||||
ips.forEach(ip => {
|
||||
countMap.set(ip, (countMap.get(ip) || 0) + 1)
|
||||
})
|
||||
|
||||
// Convert the Map into an array of objects with `str` and `cnt` properties
|
||||
const result = Array.from(countMap, ([ip, cnt]) => ({ ip, cnt }))
|
||||
|
||||
// Sort the results by the `cnt` property in descending order
|
||||
result.sort((a, b) => b.cnt - a.cnt)
|
||||
|
||||
// Ensure the result has at most 20 elements
|
||||
return result.slice(0, 20)
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getTopIps()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the top 20 most consumed endpoints.
|
||||
getTopEndpoints () {
|
||||
try {
|
||||
const endpoints = restCalls.map(x => `${x.method} ${x.url}`)
|
||||
|
||||
// Create a Map to count occurrences of each IP address string
|
||||
const countMap = new Map()
|
||||
endpoints.forEach(endpoint => {
|
||||
countMap.set(endpoint, (countMap.get(endpoint) || 0) + 1)
|
||||
})
|
||||
|
||||
// Convert the Map into an array of objects with `str` and `cnt` properties
|
||||
const result = Array.from(countMap, ([endpoint, cnt]) => ({ endpoint, cnt }))
|
||||
|
||||
// Sort the results by the `cnt` property in descending order
|
||||
result.sort((a, b) => b.cnt - a.cnt)
|
||||
|
||||
// Ensure the result has at most 20 elements
|
||||
return result.slice(0, 20)
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getTopEndpoints()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the usage database data
|
||||
async clearUsage () {
|
||||
try {
|
||||
await this.UsageModel.deleteMany({})
|
||||
|
||||
// Debugging: verify the database is empty
|
||||
// Delete this code after debugging
|
||||
const usage = await this.UsageModel.find({})
|
||||
console.log('clearUsage() usage: ', usage)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/clearUsage()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the usage data to the database
|
||||
async saveUsage (inObj = {}) {
|
||||
try {
|
||||
for (let i = 0; i < restCalls.length; i++) {
|
||||
const thisRestCall = restCalls[i]
|
||||
|
||||
// Debugging: delete this code after debugging
|
||||
// if (i === 5) {
|
||||
// console.log('saveUsage() thisRestCall: ', thisRestCall)
|
||||
// }
|
||||
|
||||
const usageData = {
|
||||
ip: thisRestCall.ip,
|
||||
url: thisRestCall.url,
|
||||
method: thisRestCall.method,
|
||||
timestamp: thisRestCall.timestamp
|
||||
}
|
||||
|
||||
const usage = new this.UsageModel(usageData)
|
||||
await usage.save()
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/saveUsage()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Load usage data from the database
|
||||
async loadUsage () {
|
||||
try {
|
||||
const usage = await this.UsageModel.find({})
|
||||
// console.log('usage: ', usage)
|
||||
|
||||
// Debugging: delete this code after debugging
|
||||
if (usage[5]) {
|
||||
console.log('loadUsage() usage[5]: ', usage[5])
|
||||
}
|
||||
|
||||
restCalls = usage
|
||||
return usage
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/loadUsage(): ', err)
|
||||
return false
|
||||
// throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This Koa middleware is called any time there is a REST API. It logs the
|
||||
// details from the request object.
|
||||
function usageMiddleware () {
|
||||
return async (ctx, next) => {
|
||||
try {
|
||||
await next()
|
||||
|
||||
// console.log('ctx.request: ', ctx.request)
|
||||
const now = new Date()
|
||||
|
||||
const reqObj = {
|
||||
ip: ctx.request.ip,
|
||||
url: ctx.request.url,
|
||||
method: ctx.request.method,
|
||||
timestamp: now.getTime()
|
||||
}
|
||||
// console.log('reqObj: ', reqObj)
|
||||
|
||||
restCalls.push(reqObj)
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500
|
||||
ctx.body = err.message
|
||||
ctx.app.emit('error', err, ctx)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { UsageUseCases, usageMiddleware, restCalls }
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Liveness test runs before all other tests to ensure the server is running.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import axios from 'axios'
|
||||
import testUtils from '../../utils/test-utils.js'
|
||||
|
||||
// const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
import config from '../../../config/index.js'
|
||||
// import Server from '../../../bin/server.js'
|
||||
// import testUtils from '../../utils/test-utils.js'
|
||||
// const adminLib = new AdminLib()
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
describe('#Check Server Liveness', () => {
|
||||
// before(async () => {
|
||||
it('should confirm the server is running', async () => {
|
||||
try {
|
||||
const response = await axios.get(`${LOCALHOST}/`)
|
||||
assert(response.status === 200, 'Server is running, continuing with E2E tests.')
|
||||
} catch (err) {
|
||||
console.log('\nServer is not running, exiting tests.')
|
||||
console.log('Start the server with `npm run start:e2e:server` before running E2E tests.\n')
|
||||
console.log('Ensure running npm run docs before running the test server')
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
it('should confirm the server is running over test enviroment', async () => {
|
||||
try {
|
||||
const res = await testUtils.loginAdminUser()
|
||||
assert.property(res, 'user')
|
||||
assert.property(res, 'token')
|
||||
assert.property(res, 'id')
|
||||
} catch (err) {
|
||||
console.log('\nServer is not running over test enviroment, exiting tests.')
|
||||
console.log('Start the server with `npm run start:e2e:server` before running E2E tests.\n')
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -13,24 +13,23 @@ import axios from 'axios'
|
||||
|
||||
// Local support libraries
|
||||
import config from '../../../config/index.js'
|
||||
|
||||
import Server from '../../../bin/server.js'
|
||||
// import Server from '../../../bin/server.js'
|
||||
import testUtils from '../../utils/test-utils.js'
|
||||
import AdminLib from '../../../src/adapters/admin.js'
|
||||
const adminLib = new AdminLib()
|
||||
|
||||
// const request = supertest.agent(app.listen())
|
||||
const context = {}
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
try {
|
||||
const app = new Server()
|
||||
if (!config.noMongo) {
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
// const app = new Server()
|
||||
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
// await app.startServer()
|
||||
|
||||
// TODO:
|
||||
|
||||
// Stop the IPFS node for the rest of the e2e tests.
|
||||
// await app.controllers.adapters.ipfs.stop()
|
||||
@@ -38,9 +37,6 @@ describe('Auth', () => {
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
@@ -51,82 +47,80 @@ describe('Auth', () => {
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
} catch (err) {
|
||||
console.log('err in auth beforeAll: ', err)
|
||||
}
|
||||
})
|
||||
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,115 +0,0 @@
|
||||
import { assert } from 'chai'
|
||||
import Admin from '../../../src/adapters/admin.js'
|
||||
import sinon from 'sinon'
|
||||
import util from 'util'
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('Admin', () => {
|
||||
beforeEach(() => {
|
||||
uut = new Admin()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('loginAdmin()', () => {
|
||||
it('should logind admin', async () => {
|
||||
try {
|
||||
const error = new Error('test error')
|
||||
error.response = {
|
||||
status: 422
|
||||
}
|
||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
const user = result.data.user
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'type')
|
||||
|
||||
assert.isString(user._id)
|
||||
assert.isString(user.email)
|
||||
assert.isString(user.type)
|
||||
|
||||
assert.equal(user.type, 'admin')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
assert.include(err.response.data, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle errors when remove user', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
End-to-end tests for /usage endpoints.
|
||||
*/
|
||||
|
||||
import config from '../../../config/index.js'
|
||||
import { assert } from 'chai'
|
||||
import axios from 'axios'
|
||||
import sinon from 'sinon'
|
||||
import util from 'util'
|
||||
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
let sandbox
|
||||
|
||||
describe('Usage', () => {
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('GET /usage', () => {
|
||||
it('should return usage status', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'status')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /usage/ips', () => {
|
||||
it('should return ips', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage/ips`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'ips')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /usage/endpoints', () => {
|
||||
it('should return ips', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage/endpoints`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'endpoints')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -14,13 +14,14 @@ describe('#adapters', () => {
|
||||
let uut, sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new Adapters()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
uut = new Adapters()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
if (sandbox) {
|
||||
sandbox.restore()
|
||||
}
|
||||
})
|
||||
|
||||
describe('#start', () => {
|
||||
@@ -37,6 +38,21 @@ describe('#adapters', () => {
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
it('should not start ipfs on test enviroment', async () => {
|
||||
// Mock dependencies
|
||||
uut.config.getJwtAtStartup = true
|
||||
uut.config.useIpfs = true
|
||||
uut.config.env = 'test'
|
||||
|
||||
sandbox.stub(uut.fullStackJwt, 'getJWT').resolves()
|
||||
sandbox.stub(uut.fullStackJwt, 'instanceBchjs').resolves()
|
||||
const ipfsSpy = sandbox.stub(uut.ipfs, 'start').resolves(null)
|
||||
|
||||
const result = await uut.start()
|
||||
|
||||
assert.isTrue(ipfsSpy.notCalled)
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { assert } from 'chai'
|
||||
import Admin from '../../../src/adapters/admin.js'
|
||||
import sinon from 'sinon'
|
||||
import util from 'util'
|
||||
import config from '../../../config/index.js'
|
||||
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('Admin', () => {
|
||||
beforeEach(() => {
|
||||
uut = new Admin()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
if (!config.noMongo) {
|
||||
describe('loginAdmin()', () => {
|
||||
it('should login admin', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'pass' })
|
||||
sandbox.stub(uut.axios, 'request').resolves(true)
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
assert.isTrue(result)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox.stub(uut.axios, 'request').throws(new Error('test error'))
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
await uut.deleteExistingSystemUser()
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
it('should update admin password', async () => {
|
||||
try {
|
||||
uut.config.adminPassword = 'newpassword'
|
||||
|
||||
const fakeUser = {
|
||||
password: 'oldpassword',
|
||||
save: () => { return 'token' },
|
||||
generateToken: () => { return 'token' }
|
||||
}
|
||||
|
||||
sandbox.stub(uut.User, 'findOne').resolves(fakeUser)
|
||||
sandbox.stub(uut.jsonFiles, 'writeJSON').resolves(true)
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
|
||||
assert.equal(fakeUser.password, 'newpassword', 'password should be updated')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.User, 'findOne').throws(new Error('test error'))
|
||||
await uut.createSystemUser()
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteExistingSystemUser()', () => {
|
||||
it('should delete admin', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.User, 'deleteOne').resolves(true)
|
||||
const result = await uut.deleteExistingSystemUser()
|
||||
assert.isTrue(result)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error when deleting admin', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
await uut.deleteExistingSystemUser()
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -16,7 +16,7 @@ describe('#IPFS-adapter-index', () => {
|
||||
let bchjs
|
||||
|
||||
beforeEach(() => {
|
||||
bchjs = new BCHJS()
|
||||
bchjs = new BCHJS({ restURL: 'https://api.fullstack.cash/v5/' })
|
||||
uut = new IPFSLib({ bchjs })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
@@ -5,26 +5,17 @@
|
||||
// Global npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import { peerIdFromString } from '@libp2p/peer-id'
|
||||
|
||||
// Local libraries
|
||||
import IPFSLib from '../../../src/adapters/ipfs/ipfs.js'
|
||||
// import create from '../mocks/ipfs-mock.js'
|
||||
import config from '../../../config/index.js'
|
||||
import createHeliaLib from '../mocks/helia-mock.js'
|
||||
|
||||
// config.isProduction = true;
|
||||
describe('#IPFS-adapter', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
let ipfs
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new IPFSLib()
|
||||
|
||||
ipfs = cloneDeep(createHeliaLib)
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
@@ -39,26 +30,37 @@ describe('#IPFS-adapter', () => {
|
||||
assert.isFunction(_uut.start)
|
||||
assert.isFunction(_uut.stop)
|
||||
})
|
||||
|
||||
it('should instantiate dev IPFS Lib in production mode.', async () => {
|
||||
config.isProduction = true
|
||||
const _uut = new IPFSLib()
|
||||
assert.exists(_uut)
|
||||
assert.isFunction(_uut.start)
|
||||
assert.isFunction(_uut.stop)
|
||||
config.isProduction = false
|
||||
})
|
||||
})
|
||||
|
||||
describe('#start', () => {
|
||||
it('should return a promise that resolves into an instance of IPFS.', async () => {
|
||||
// Mock dependencies.
|
||||
sandbox.stub(uut, 'createNode').resolves(ipfs)
|
||||
sandbox.stub(uut, 'publicIp').resolves('192.168.2.4')
|
||||
sandbox.stub(uut, 'multiaddr').returns('/ip4/fake-multiaddr')
|
||||
// Mock the heliaNode that gets created inside start()
|
||||
const mockIpfs = {
|
||||
libp2p: {
|
||||
getMultiaddrs: () => [],
|
||||
peerId: 'fake-id'
|
||||
}
|
||||
}
|
||||
|
||||
// Override the start method to mock the heliaNode creation
|
||||
uut.start = async function () {
|
||||
this.heliaNode = {
|
||||
start: async () => mockIpfs,
|
||||
id: 'fake-id',
|
||||
multiaddrs: ['/ip4/fake-multiaddr']
|
||||
}
|
||||
|
||||
const ipfs = await this.heliaNode.start()
|
||||
|
||||
this.id = this.heliaNode.id
|
||||
this.multiaddrs = this.heliaNode.multiaddrs
|
||||
this.isReady = true
|
||||
this.ipfs = ipfs
|
||||
|
||||
return this.ipfs
|
||||
}
|
||||
|
||||
const result = await uut.start()
|
||||
// console.log('result: ', result)
|
||||
|
||||
// Assert properties of the instance are set.
|
||||
assert.equal(uut.isReady, true)
|
||||
@@ -71,8 +73,9 @@ describe('#IPFS-adapter', () => {
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'createNode').rejects(new Error('test error'))
|
||||
// Force an error by making getSeed throw, which will cause
|
||||
// CreateHeliaNode to fail during start.
|
||||
sandbox.stub(uut, 'getSeed').rejects(new Error('test error'))
|
||||
|
||||
await uut.start()
|
||||
assert.fail('Unexpected code path.')
|
||||
@@ -97,119 +100,12 @@ describe('#IPFS-adapter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#ensureBlocksDir', () => {
|
||||
it('should create directory if it does not exist', () => {
|
||||
// Force desired code path
|
||||
sandbox.stub(uut.fs, 'existsSync').returns(false)
|
||||
sandbox.stub(uut.fs, 'mkdirSync').returns(true)
|
||||
|
||||
const result = uut.ensureBlocksDir()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should report and throw errors', () => {
|
||||
// Force an error
|
||||
sandbox.stub(uut.fs, 'existsSync').throws(new Error('test error'))
|
||||
|
||||
try {
|
||||
uut.ensureBlocksDir()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createNode', () => {
|
||||
it('should report and throw errors', async () => {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'createLibp2p').rejects(new Error('test error'))
|
||||
|
||||
try {
|
||||
await uut.createNode()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should create an IPFS node from Helia', async () => {
|
||||
uut.config.isCircuitRelay = false
|
||||
const result = await uut.createNode()
|
||||
// console.log('result: ', result)
|
||||
|
||||
// Assert the returned IPFS node has expected properties
|
||||
assert.property(result, 'libp2p')
|
||||
assert.property(result, 'blockstore')
|
||||
|
||||
// Stop the IPFS node
|
||||
await result.stop()
|
||||
})
|
||||
|
||||
it('should create a Circuit Relay if configured', async () => {
|
||||
uut.config.isCircuitRelay = true
|
||||
const result = await uut.createNode()
|
||||
|
||||
// Assert the returned IPFS node has expected properties
|
||||
assert.property(result, 'libp2p')
|
||||
assert.property(result, 'blockstore')
|
||||
|
||||
// Stop the IPFS node
|
||||
await result.stop()
|
||||
})
|
||||
|
||||
it('should create a new private key on first run', async () => {
|
||||
uut.config.isCircuitRelay = false
|
||||
|
||||
// Mock dependencies and force desired code path.
|
||||
let beenCalled = false
|
||||
sandbox.stub(uut, 'getKeychain').resolves({
|
||||
exportPeerId: async () => {
|
||||
if (!beenCalled) {
|
||||
beenCalled = true
|
||||
throw new Error('test error')
|
||||
}
|
||||
return peerIdFromString('12D3KooWSXF1PnEfiA8bCG8SJduCvzdwHtvhVPK4WC6zzDoto2XP')
|
||||
},
|
||||
createKey: async () => {}
|
||||
})
|
||||
sandbox.stub(uut, 'createLibp2p').resolves()
|
||||
sandbox.stub(uut, 'createHelia').resolves({})
|
||||
|
||||
const result = await uut.createNode()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'fs')
|
||||
})
|
||||
|
||||
it('should not use circuit relay when CONNECT_PREF is set to direct', async () => {
|
||||
process.env.CONNECT_PREF = 'direct'
|
||||
|
||||
uut.config.isCircuitRelay = false
|
||||
const result = await uut.createNode()
|
||||
// console.log('result: ', result)
|
||||
|
||||
delete process.env.CONNECT_PREF
|
||||
|
||||
// Assert the returned IPFS node has expected properties
|
||||
assert.property(result, 'libp2p')
|
||||
assert.property(result, 'blockstore')
|
||||
|
||||
// Stop the IPFS node
|
||||
await result.stop()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getSeed', () => {
|
||||
it('should read the seed from the JSON file', async () => {
|
||||
// Mock dependencies and force desired code path
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves('12345678')
|
||||
|
||||
const result = await uut.getSeed()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
@@ -220,7 +116,6 @@ describe('#IPFS-adapter', () => {
|
||||
sandbox.stub(uut.jsonFiles, 'writeJSON').resolves()
|
||||
|
||||
const result = await uut.getSeed()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
|
||||
@@ -41,5 +41,17 @@ describe('#passport.js', () => {
|
||||
assert.include(err.message, 'cant auth user')
|
||||
}
|
||||
})
|
||||
it('should authenticate user', async () => {
|
||||
const ctx = {}
|
||||
const errMock = null
|
||||
const userMock = {
|
||||
_id: '123',
|
||||
email: 'test@test.com'
|
||||
}
|
||||
sandbox.stub(uut.passport, 'authenticate').yields(errMock, userMock)
|
||||
const user = await uut.authUser(ctx)
|
||||
assert.equal(user._id, userMock._id)
|
||||
assert.equal(user.email, userMock.email)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,6 +54,13 @@ describe('#User-Adapter', () => {
|
||||
|
||||
assert.notEqual(testuser.password, 'password')
|
||||
})
|
||||
it('should ignore password encrption if password property is not provided', async () => {
|
||||
const lastPassword = testuser.password
|
||||
await testuser.save()
|
||||
// console.log('testuser: ', testuser)
|
||||
|
||||
assert.equal(testuser.password, lastPassword)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#validatePassword', () => {
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
Unit tests for the Wallet Adapter library.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
import fs from 'fs'
|
||||
|
||||
// Local libraries
|
||||
import WalletAdapter from '../../../src/adapters/wallet.adapter.js'
|
||||
import { MockBchWallet } from '../mocks/adapters/wallet.js'
|
||||
|
||||
// Hack to get __dirname back.
|
||||
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
||||
import * as url from 'url'
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
// Global constants
|
||||
const testWalletFile = `${__dirname.toString()}test-wallet.json`
|
||||
|
||||
describe('#wallet', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
before(() => {
|
||||
// Delete the test file if it exists.
|
||||
try {
|
||||
deleteFile(testWalletFile)
|
||||
} catch (err) { }
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new WalletAdapter()
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
after(() => {
|
||||
// Delete the test file if it exists.
|
||||
try {
|
||||
deleteFile(testWalletFile)
|
||||
} catch (err) { }
|
||||
})
|
||||
|
||||
describe('#_instanceWallet', () => {
|
||||
it('should create a wallet given a mnemonic', async () => {
|
||||
const mnemonic = 'wagon tray learn flat erase laugh lonely rug check captain jacket morning'
|
||||
const result = await uut._instanceWallet(mnemonic)
|
||||
// console.log('result: ', result)
|
||||
assert.equal(result.walletInfo.mnemonic, mnemonic)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#openWallet', () => {
|
||||
it('should create a new wallet when wallet file does not exist', async () => {
|
||||
// Mock dependencies
|
||||
uut.BchWallet = MockBchWallet
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const result = await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
assert.property(result, 'mnemonic')
|
||||
assert.property(result, 'privateKey')
|
||||
assert.property(result, 'publicKey')
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'address')
|
||||
assert.property(result, 'slpAddress')
|
||||
assert.property(result, 'legacyAddress')
|
||||
assert.property(result, 'hdPath')
|
||||
})
|
||||
|
||||
it('should open existing wallet file', async () => {
|
||||
// This test case uses the file created in the previous test case.
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const result = await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
assert.property(result, 'mnemonic')
|
||||
assert.property(result, 'privateKey')
|
||||
assert.property(result, 'publicKey')
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'address')
|
||||
assert.property(result, 'slpAddress')
|
||||
assert.property(result, 'legacyAddress')
|
||||
assert.property(result, 'hdPath')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
uut.config.walletFile = ''
|
||||
uut.BchWallet = () => {
|
||||
}
|
||||
await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'this.BchWallet is not a constructor')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#instanceWalletWithoutInitialization', () => {
|
||||
it('should create an instance of BchWallet', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet(undefined, { restURL: 'https://api.fullstack.cash/v5/' })
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
uut.config.authPass = 'fake-auth-pass'
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
|
||||
// console.log('walletData: ', walletData)
|
||||
const result = await uut.instanceWalletWithoutInitialization(walletData)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, '_instanceWallet').rejects(new Error('test error'))
|
||||
|
||||
await uut.instanceWalletWithoutInitialization()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should create an instance of BchWallet using web2 infra', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet(undefined, { restURL: 'https://api.fullstack.cash/v5/' })
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
|
||||
// Force desired code path
|
||||
uut.config.walletInterface = 'web2'
|
||||
const result = await uut.instanceWalletWithoutInitialization(walletData)
|
||||
|
||||
// console.log('result: ', result)
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
|
||||
it('should generate wallet from mnemonic in config', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet(undefined, { restURL: 'https://api.fullstack.cash/v5/' })
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
|
||||
const originalConfig = uut.config.mnemonic
|
||||
uut.config.mnemonic = walletData.mnemonic
|
||||
|
||||
const result = await uut.instanceWalletWithoutInitialization({})
|
||||
// console.log('result: ', result)
|
||||
|
||||
uut.config.mnemonic = originalConfig
|
||||
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#instanceWallet', () => {
|
||||
it('should create an instance of BchWallet', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet(undefined, { restURL: 'https://api.fullstack.cash/v5/' })
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.WALLET_FILE = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
const result = await uut.instanceWallet(walletData)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'instanceWalletWithoutInitialization').rejects(new Error('test error'))
|
||||
|
||||
await uut.instanceWallet()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should create an instance of BchWallet using web2 infra', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet(undefined, { restURL: 'https://api.fullstack.cash/v5/' })
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.WALLET_FILE = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
|
||||
// Force desired code path
|
||||
uut.config.useFullStackCash = true
|
||||
const result = await uut.instanceWallet(walletData)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#incrementNextAddress', () => {
|
||||
it('should increment the nextAddress property', async () => {
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.WALLET_FILE = testWalletFile
|
||||
// mock instance of minimal-slp-wallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
const result = await uut.incrementNextAddress()
|
||||
assert.equal(result, 2)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'openWallet').rejects(new Error('test error'))
|
||||
await uut.incrementNextAddress()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getKeyPair', () => {
|
||||
it('should return an object with a key pair', async () => {
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
// uut.WALLET_FILE = testWalletFile
|
||||
uut.config.walletFile = testWalletFile
|
||||
|
||||
const result = await uut.getKeyPair()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'wif')
|
||||
assert.property(result, 'hdIndex')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut, 'incrementNextAddress')
|
||||
.rejects(new Error('test error'))
|
||||
await uut.getKeyPair()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#optimize', () => {
|
||||
it('should call the wallet optimize function', async () => {
|
||||
// mock instance of minimal-slp-wallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
sandbox.stub(uut.bchWallet, 'optimize').resolves({ bchUtxoCnt: 10 })
|
||||
const result = await uut.optimize()
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getBalance', () => {
|
||||
it('should get the balance for the wallet', async () => {
|
||||
// mock instance of minimal-slp-wallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
// Mock dependencies and force desired code path
|
||||
sandbox.stub(uut.bchWallet, 'getBalance').resolves(41012)
|
||||
sandbox.stub(uut.bchWallet, 'listTokens').resolves([{
|
||||
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
ticker: 'PSF',
|
||||
name: 'Permissionless Software Foundation',
|
||||
decimals: 8,
|
||||
tokenType: 1,
|
||||
url: 'psfoundation.cash',
|
||||
qty: 2
|
||||
}])
|
||||
const result = await uut.getBalance()
|
||||
// console.log('result: ', result)
|
||||
// Assert the expected properties exist and have the expected values.
|
||||
assert.equal(result.satBalance, 41012)
|
||||
assert.equal(result.psfBalance, 2)
|
||||
assert.equal(result.success, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const deleteFile = (filepath) => {
|
||||
try {
|
||||
// Delete state if exist
|
||||
fs.unlinkSync(filepath)
|
||||
} catch (err) {
|
||||
// console.error('Error trying to delete file: ', err)
|
||||
}
|
||||
}
|
||||
@@ -63,4 +63,25 @@ describe('#Controllers', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#attachRESTControllers', () => {
|
||||
it('should attach the controllers', async () => {
|
||||
const app = {
|
||||
use: () => {}
|
||||
}
|
||||
|
||||
await uut.attachRESTControllers(app)
|
||||
})
|
||||
})
|
||||
describe('#initAdapters', () => {
|
||||
it('should attach the controllers', async () => {
|
||||
sandbox.stub(uut.adapters, 'start').resolves({})
|
||||
await uut.initAdapters()
|
||||
})
|
||||
})
|
||||
describe('#initUseCases', () => {
|
||||
it('should attach the controllers', async () => {
|
||||
sandbox.stub(uut.useCases, 'start').resolves({})
|
||||
await uut.initUseCases()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -79,11 +79,22 @@ describe('#Auth-REST-Router', () => {
|
||||
await uut.authUser(ctx)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
it('should catch and throw passport error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.passport, 'authUser').rejects('test error')
|
||||
|
||||
await uut.authUser(ctx)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
it('should handle error if user is not found!', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.passport, 'authUser').resolves(null)
|
||||
|
||||
await uut.authUser(ctx)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Unit tests for the REST API middleware that handle response errors.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local libraries
|
||||
import errorMiddleware from '../../../../../src/controllers/rest-api/middleware/error.js'
|
||||
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
|
||||
|
||||
describe('#Validators', () => {
|
||||
let ctx
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#errorMiddleware', () => {
|
||||
it('should run next function', async () => {
|
||||
// Spy on next
|
||||
const next = sinon.spy(() => { })
|
||||
errorMiddleware()(ctx, next)
|
||||
|
||||
assert.isTrue(next.calledOnce)
|
||||
})
|
||||
|
||||
it('should handle unknown status error', async () => {
|
||||
try {
|
||||
const next = async () => {
|
||||
const e = new Error('test error')
|
||||
e.status = null
|
||||
throw e
|
||||
}
|
||||
|
||||
await errorMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(ctx.status, 500)
|
||||
assert.equal(ctx.body, 'test error')
|
||||
}
|
||||
})
|
||||
it('should handle known status error', async () => {
|
||||
try {
|
||||
const next = async () => {
|
||||
const e = new Error('test error')
|
||||
e.status = 422
|
||||
throw e
|
||||
}
|
||||
|
||||
await errorMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(ctx.status, 422)
|
||||
assert.equal(ctx.body, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -20,7 +20,7 @@ describe('#RESTControllers', () => {
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
before(async () => {})
|
||||
before(async () => { })
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
@@ -64,4 +64,19 @@ describe('#RESTControllers', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attachRESTControllers', () => {
|
||||
it('should attach controllers without mongo service', () => {
|
||||
uut.config.noMongo = true
|
||||
|
||||
const app = { use: () => {} }
|
||||
uut.attachRESTControllers(app)
|
||||
})
|
||||
it('should attach controllers with mongo service', () => {
|
||||
uut.config.noMongo = false
|
||||
|
||||
const app = { use: () => {} }
|
||||
uut.attachRESTControllers(app)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /usage 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 UsageController from '../../../../../src/controllers/rest-api/usage/controller.js'
|
||||
|
||||
import { context as mockContext } from '../../../mocks/ctx-mock.js'
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
describe('#Usage-REST-Controller', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new UsageController({ 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 UsageController()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageController({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /usage', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getRestSummary').throws(new Error('test error'))
|
||||
await uut.getStatus(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getStatus(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'status')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /ips', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getTopIps').throws(new Error('test error'))
|
||||
await uut.getTopIps(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getTopIps(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'ips')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /endpoints', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getTopEndpoints').throws(new Error('test error'))
|
||||
await uut.getTopEndpoints(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getTopEndpoints(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'endpoints')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleError', () => {
|
||||
it('should pass an error message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 422,
|
||||
message: 'Unprocessable Entity'
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unprocessable Entity')
|
||||
}
|
||||
})
|
||||
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')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /usage 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 UsageRouter from '../../../../../src/controllers/rest-api/usage/index.js'
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Usage-REST-Router', () => {
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new UsageRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating IPFS 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 attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -153,6 +153,20 @@ describe('#Users-REST-Controller', () => {
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'user')
|
||||
})
|
||||
it('should run next function if it exists', async () => {
|
||||
// Mock dependencies
|
||||
const nextSpy = sandbox.spy()
|
||||
sandbox.stub(uut.useCases.user, 'getUser').resolves({ _id: '123' })
|
||||
|
||||
await uut.getUser(ctx, nextSpy)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'user')
|
||||
assert.isTrue(nextSpy.calledOnce)
|
||||
})
|
||||
|
||||
it('should return other error status passed by biz logic', async () => {
|
||||
try {
|
||||
|
||||
@@ -79,4 +79,70 @@ describe('#Users-REST-Router', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createUser', () => {
|
||||
it('should ignore admin validator when DISABLE_NEW_ACCOUNTS is not defined', async () => {
|
||||
// Stub functions
|
||||
const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
|
||||
sandbox.stub(uut.userRESTController, 'createUser').resolves(true)
|
||||
|
||||
// Call function
|
||||
await uut.createUser()
|
||||
|
||||
// Assertions
|
||||
assert.isTrue(validationSpy.notCalled, 'Admin validator should not be called')
|
||||
})
|
||||
it('should ensure admin when DISABLE_NEW_ACCOUNTS is defined', async () => {
|
||||
// Set environment variable
|
||||
process.env.DISABLE_NEW_ACCOUNTS = true
|
||||
|
||||
// Stub functions
|
||||
const validationSpy = sandbox.stub(uut.validators, 'ensureAdmin').resolves(true)
|
||||
sandbox.stub(uut.userRESTController, 'createUser').resolves(true)
|
||||
|
||||
// Call function
|
||||
await uut.createUser()
|
||||
|
||||
// Assertions
|
||||
assert.isTrue(validationSpy.calledOnce, 'Admin validator should be called')
|
||||
})
|
||||
})
|
||||
describe('#getAll', () => {
|
||||
it('should route to controller', async () => {
|
||||
sandbox.stub(uut.validators, 'ensureUser').resolves(true)
|
||||
const spy = sandbox.stub(uut.userRESTController, 'getUsers').resolves(true)
|
||||
|
||||
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)
|
||||
|
||||
await uut.getById()
|
||||
assert.isTrue(spy.calledOnce)
|
||||
})
|
||||
})
|
||||
describe('#updateUser', () => {
|
||||
it('should route to controller', async () => {
|
||||
sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
|
||||
sandbox.stub(uut.userRESTController, 'getUser').resolves(true)
|
||||
const spy = sandbox.stub(uut.userRESTController, 'updateUser').resolves(true)
|
||||
|
||||
await uut.updateUser()
|
||||
assert.isTrue(spy.calledOnce)
|
||||
})
|
||||
})
|
||||
describe('#deleteUser', () => {
|
||||
it('should route to controller', async () => {
|
||||
sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
|
||||
sandbox.stub(uut.userRESTController, 'getUser').resolves(true)
|
||||
const spy = sandbox.stub(uut.userRESTController, 'deleteUser').resolves(true)
|
||||
|
||||
await uut.deleteUser()
|
||||
assert.isTrue(spy.calledOnce)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -66,15 +66,47 @@ describe('#Timer-Controllers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#exampleTimerFunc', () => {
|
||||
// describe('#exampleTimerFunc', () => {
|
||||
// it('should kick off the Use Case', async () => {
|
||||
// const result = await uut.exampleTimerFunc()
|
||||
|
||||
// assert.equal(result, true)
|
||||
// })
|
||||
|
||||
// it('should return false on error', async () => {
|
||||
// const result = await uut.exampleTimerFunc(true)
|
||||
|
||||
// assert.equal(result, false)
|
||||
// })
|
||||
// })
|
||||
|
||||
describe('#cleanUsage', () => {
|
||||
it('should kick off the Use Case', async () => {
|
||||
const result = await uut.exampleTimerFunc()
|
||||
const result = await uut.cleanUsage()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
const result = await uut.exampleTimerFunc(true)
|
||||
sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error'))
|
||||
const result = await uut.cleanUsage()
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#backupUsage', () => {
|
||||
it('should kick off the Use Case', async () => {
|
||||
const result = await uut.backupUsage()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
sandbox.stub(uut.useCases.usage, 'clearUsage').throws(new Error('test error'))
|
||||
// sandbox.stub(uut.useCases.usage, 'saveUsage').throws(new Error('test error'))
|
||||
|
||||
const result = await uut.backupUsage()
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
|
||||
@@ -10,15 +10,23 @@ describe('#config', () => {
|
||||
before(() => {
|
||||
// Backup the current environment setting.
|
||||
currentEnv = process.env.SVC_ENV
|
||||
// Clear SVC_ENV for the first test to ensure default behavior
|
||||
delete process.env.SVC_ENV
|
||||
})
|
||||
|
||||
after(() => {
|
||||
// Restore the environment setting before starting these tests.
|
||||
process.env.SVC_ENV = currentEnv
|
||||
if (currentEnv) {
|
||||
process.env.SVC_ENV = currentEnv
|
||||
} else {
|
||||
delete process.env.SVC_ENV
|
||||
}
|
||||
})
|
||||
|
||||
it('Should return development environment config by default', async () => {
|
||||
const importedConfig = await import('../../../config/index.js')
|
||||
// Ensure SVC_ENV is not set for this test
|
||||
delete process.env.SVC_ENV
|
||||
const importedConfig = await import('../../../config/index.js?foo=bar0')
|
||||
const config = importedConfig.default
|
||||
// console.log('config: ', config)
|
||||
|
||||
@@ -38,9 +46,13 @@ describe('#config', () => {
|
||||
assert.equal(config.env, 'test')
|
||||
})
|
||||
|
||||
it('Should return test environment config', async () => {
|
||||
it('Should return prod environment config', async () => {
|
||||
process.env.SVC_ENV = 'prod'
|
||||
|
||||
process.env.WALLET_INTERFACE = 'web2'
|
||||
process.env.APISERVER = 'https://api.fullstack.cash/v5/'
|
||||
|
||||
await import('../../../config/env/common.js?foo=bar2')
|
||||
const importedConfig3 = await import('../../../config/index.js?foo=bar2')
|
||||
const config = importedConfig3.default
|
||||
// console.log('config: ', config)
|
||||
|
||||
@@ -34,6 +34,12 @@ describe('#passport', () => {
|
||||
|
||||
passportCallback(id, 'password', done)
|
||||
})
|
||||
it('should handle not found user', () => {
|
||||
// Mock Users model.
|
||||
sandbox.stub(User, 'findOne').resolves(null)
|
||||
|
||||
passportCallback(id, 'password', done)
|
||||
})
|
||||
|
||||
it('should return if password is validated', () => {
|
||||
// Mock Users model.
|
||||
@@ -41,6 +47,15 @@ describe('#passport', () => {
|
||||
|
||||
passportCallback(id, 'password', done)
|
||||
})
|
||||
it('should handle error on password validation', () => {
|
||||
// Mock Users model.
|
||||
const userMock = {
|
||||
validatePassword: () => false
|
||||
}
|
||||
sandbox.stub(User, 'findOne').resolves(userMock)
|
||||
|
||||
passportCallback(id, 'password', done)
|
||||
})
|
||||
|
||||
it('should catch a high-level error', () => {
|
||||
// Force an error
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('#server', () => {
|
||||
sandbox.stub(uut.adminLib, 'createSystemUser').resolves(true)
|
||||
sandbox.stub(uut.controllers, 'attachControllers').resolves()
|
||||
uut.config.env = 'dev'
|
||||
|
||||
uut.config.port = 5040
|
||||
const result = await uut.startServer()
|
||||
// console.log('result: ', result)
|
||||
|
||||
|
||||
@@ -70,6 +70,39 @@ const localdb = {
|
||||
}
|
||||
},
|
||||
|
||||
Usage: class Usage {
|
||||
static findById () {}
|
||||
static find () {}
|
||||
static findOne () {
|
||||
return {
|
||||
validatePassword: localdb.validatePassword
|
||||
}
|
||||
}
|
||||
|
||||
async save () {
|
||||
return {}
|
||||
}
|
||||
|
||||
generateToken () {
|
||||
return '123'
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
return {}
|
||||
}
|
||||
|
||||
async remove () {
|
||||
return true
|
||||
}
|
||||
|
||||
async validatePassword () {
|
||||
return true
|
||||
}
|
||||
static async deleteMany(){
|
||||
return true
|
||||
}
|
||||
},
|
||||
|
||||
validatePassword: () => {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import BCHJS from "@psf/bch-js";
|
||||
|
||||
const mockWallet = {
|
||||
mnemonic: 'course abstract aerobic deer try switch turtle diet fence affair butter top',
|
||||
privateKey: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
|
||||
publicKey: '0379433ffc401483ade310469953c1cba77c71af904f07c15bde330d7198b4d6dc',
|
||||
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
address: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
slpAddress: 'simpleledger:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acq9zm7d33',
|
||||
legacyAddress: '1JQj1KcQL7GPKzc1D2PvdUSgw3MbDtrHzi',
|
||||
hdPath: "m/44'/245'/0'/0/0",
|
||||
nextAddress: 1
|
||||
};
|
||||
|
||||
class MockBchWallet {
|
||||
constructor() {
|
||||
this.walletInfoPromise = true;
|
||||
this.walletInfo = mockWallet;
|
||||
this.initialize = async () => {}
|
||||
this.bchjs = new BCHJS({ restURL: 'https://api.fullstack.cash/v5/' });
|
||||
this.burnTokens = async () => {
|
||||
return { success: true, txid: 'txid' };
|
||||
};
|
||||
this.sendTokens = async () => {
|
||||
return 'fakeTxid';
|
||||
};
|
||||
this.getUtxos = async () => { };
|
||||
this.getBalance = async () => { };
|
||||
this.listTokens = async () => { };
|
||||
this.getTxData = async () => {
|
||||
return [{
|
||||
tokenTicker: 'TROUT'
|
||||
}];
|
||||
};
|
||||
this.optimize = async () => { };
|
||||
// Environment variable is used by wallet-balance.unit.js to force an error.
|
||||
if (process.env.NO_UTXO) {
|
||||
this.utxos = {};
|
||||
}
|
||||
else {
|
||||
this.utxos = {
|
||||
utxoStore: {
|
||||
address: 'bitcoincash:qqetvdnlt0p8g27dr44cx7h057kpzly9xse9huc97z',
|
||||
bchUtxos: [
|
||||
{
|
||||
height: 700685,
|
||||
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
tx_pos: 0,
|
||||
value: 1000,
|
||||
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
vout: 0,
|
||||
isValid: false
|
||||
},
|
||||
{
|
||||
height: 700685,
|
||||
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
tx_pos: 2,
|
||||
value: 19406,
|
||||
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
vout: 2,
|
||||
isValid: false
|
||||
}
|
||||
],
|
||||
nullUtxos: [],
|
||||
slpUtxos: {
|
||||
type1: {
|
||||
mintBatons: [],
|
||||
tokens: [
|
||||
{
|
||||
'height': 717331,
|
||||
'tx_hash': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
|
||||
'tx_pos': 1,
|
||||
'value': 546,
|
||||
'txid': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
|
||||
'vout': 1,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '10',
|
||||
'tokenId': '600ee24d0f208aebc2bdd2c4ee1b9acb6d57343561442e8676b5bbea311d5a0f',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'FLIPS',
|
||||
'name': 'FLIPS',
|
||||
'documentUri': '',
|
||||
'documentHash': '',
|
||||
'decimals': 1,
|
||||
'qtyStr': '1'
|
||||
},
|
||||
{
|
||||
'height': 730597,
|
||||
'tx_hash': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
|
||||
'tx_pos': 1,
|
||||
'value': 546,
|
||||
'txid': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
|
||||
'vout': 1,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '34999991',
|
||||
'tokenId': '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'PSF',
|
||||
'name': 'Permissionless Software Foundation',
|
||||
'documentUri': 'psfoundation.cash',
|
||||
'documentHash': '',
|
||||
'decimals': 8,
|
||||
'qtyStr': '0.34999991'
|
||||
},
|
||||
{
|
||||
'height': 730597,
|
||||
'tx_hash': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
|
||||
'tx_pos': 2,
|
||||
'value': 546,
|
||||
'txid': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
|
||||
'vout': 2,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '18898',
|
||||
'tokenId': 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'TROUT',
|
||||
'name': "Trout's test token",
|
||||
'documentUri': 'troutsblog.com',
|
||||
'documentHash': '',
|
||||
'decimals': 2,
|
||||
'qtyStr': '188.98'
|
||||
}
|
||||
]
|
||||
},
|
||||
nft: {
|
||||
tokens: []
|
||||
},
|
||||
group: {
|
||||
tokens: [],
|
||||
mintBatons: []
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MockBchWallet };
|
||||
|
||||
export { mockWallet };
|
||||
|
||||
export default {
|
||||
MockBchWallet,
|
||||
mockWallet
|
||||
};
|
||||
@@ -47,6 +47,32 @@ class BCH {
|
||||
}
|
||||
}
|
||||
|
||||
class UsageUseCaseMock {
|
||||
async cleanUsage() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async getRestSummary() {
|
||||
return true
|
||||
}
|
||||
|
||||
async getTopIps(params) {
|
||||
return true
|
||||
}
|
||||
|
||||
async getTopEndpoints(existingUser, newData) {
|
||||
return true
|
||||
}
|
||||
|
||||
async clearUsage() {
|
||||
return true
|
||||
}
|
||||
|
||||
async saveUsage() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class UseCasesMock {
|
||||
constuctor(localConfig = {}) {
|
||||
// this.user = new UserUseCaseMock(localConfig)
|
||||
@@ -54,6 +80,7 @@ class UseCasesMock {
|
||||
|
||||
user = new UserUseCaseMock()
|
||||
bch = new BCH()
|
||||
usage = new UsageUseCaseMock()
|
||||
}
|
||||
|
||||
export default UseCasesMock;
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
Unit tests for the use-cases/usage-use-cases.js business logic library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local support libraries
|
||||
import adapters from '../mocks/adapters/index.js'
|
||||
|
||||
// Mock
|
||||
import { context as mockContext } from '../mocks/ctx-mock.js'
|
||||
|
||||
// Unit under test (uut)
|
||||
import { UsageUseCases, restCalls, usageMiddleware } from '../../../src/use-cases/usage-use-cases.js'
|
||||
|
||||
describe('#usage-use-case', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
before(async () => {
|
||||
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
uut = new UsageUseCases({ adapters })
|
||||
|
||||
// Set as empty array
|
||||
restCalls.splice(0, restCalls.length)
|
||||
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageUseCases()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of adapters must be passed in when instantiating Usage Use Cases library.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#cleanUsage', () => {
|
||||
it('should delete older data than 24 hours', () => {
|
||||
const now = new Date() // Mock date
|
||||
|
||||
// set older mock data
|
||||
restCalls.push({
|
||||
timestamp: now.getTime() - (60000 * 60 * 48), // 48 hours ago
|
||||
ip: '127.0.0.1'
|
||||
})
|
||||
|
||||
// Set recently mock data
|
||||
restCalls.push({
|
||||
timestamp: now.getTime(),
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.cleanUsage()
|
||||
|
||||
assert.isArray(result)
|
||||
assert.equal(result.length, 1)
|
||||
assert.equal(result[0].ip, 'localhost')
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(restCalls, 'filter').throws(new Error('uut error'))
|
||||
|
||||
uut.cleanUsage()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRestSummary', () => {
|
||||
it('should get the number of rest calls', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.getRestSummary()
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(console, 'log').throws(new Error('uut error'))
|
||||
|
||||
uut.getRestSummary()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTopIps', () => {
|
||||
it('should get top IPs', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.getTopIps()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'ip')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result[0].ip, 'localhost')
|
||||
assert.equal(result[0].cnt, '2')
|
||||
})
|
||||
|
||||
it('should return a maximum of 20 values', () => {
|
||||
// Fill Array with 21 values
|
||||
for (let i = 0; i < 21; i++) {
|
||||
restCalls.push({
|
||||
ip: `localhost-${i}`
|
||||
})
|
||||
}
|
||||
|
||||
const result = uut.getTopIps()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'ip')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result.length, 20)
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Set mock data
|
||||
restCalls.push(null)
|
||||
|
||||
uut.getTopIps()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Cannot read properties')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTopEndpoints', () => {
|
||||
it('should get top Endpoints', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: '/api/v1/users',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: '/api/v1/users',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
const result = uut.getTopEndpoints()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'endpoint')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result[0].endpoint, 'GET /api/v1/users')
|
||||
assert.equal(result[0].cnt, '2')
|
||||
})
|
||||
|
||||
it('should return a maximum of 20 values', () => {
|
||||
// Fill Array with 21 values
|
||||
for (let i = 0; i < 21; i++) {
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: `/api/v1/users-${i}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
const result = uut.getTopEndpoints()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'endpoint')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result.length, 20)
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Set mock data
|
||||
restCalls.push(null)
|
||||
|
||||
uut.getTopEndpoints()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Cannot read properties')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#usageMiddleware', () => {
|
||||
it('should update restCalls state', async () => {
|
||||
// Spy on next
|
||||
const next = sinon.spy(() => { })
|
||||
|
||||
await usageMiddleware()(ctx, next)
|
||||
|
||||
assert.equal(restCalls.length, 1)
|
||||
assert.isTrue(next.called)
|
||||
})
|
||||
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
const next = () => { throw new Error('uut error') }
|
||||
|
||||
await usageMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
assert.equal(ctx.status, 500)
|
||||
assert.equal(restCalls.length, 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#clearUsage', () => {
|
||||
it('should clear the usage database data', async () => {
|
||||
const res = await uut.clearUsage()
|
||||
assert.isTrue(res)
|
||||
})
|
||||
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.UsageModel, 'deleteMany').throws(new Error('uut error'))
|
||||
await uut.clearUsage()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#saveUsage', () => {
|
||||
it('should save usage', async () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
timestamp: new Date().getTime(),
|
||||
ip: 'localhost',
|
||||
url: 'fakeUrl',
|
||||
method: 'unit test'
|
||||
})
|
||||
const res = await uut.saveUsage()
|
||||
assert.isTrue(res)
|
||||
})
|
||||
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
restCalls.push(null)
|
||||
await uut.saveUsage()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
assert.include(error.message, 'Cannot read properties')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#loadUsage', () => {
|
||||
it('should load usage', async () => {
|
||||
// Set mock data
|
||||
const mockObj = {
|
||||
timestamp: new Date().getTime(),
|
||||
ip: 'localhost',
|
||||
url: 'fakeUrl',
|
||||
method: 'unit test'
|
||||
}
|
||||
sandbox.stub(uut.UsageModel, 'find').returns(new Array(10).fill(null).map((_, i) => (mockObj)))
|
||||
const res = await uut.loadUsage()
|
||||
assert.equal(restCalls.length, 10)
|
||||
assert.equal(res.length, 10)
|
||||
})
|
||||
|
||||
it('should skip error', async () => {
|
||||
sandbox.stub(uut.UsageModel, 'find').throws(new Error('uut error'))
|
||||
const res = await uut.loadUsage()
|
||||
assert.isFalse(res)
|
||||
})
|
||||
})
|
||||
})
|
||||
+45
-20
@@ -8,7 +8,6 @@ import axios from 'axios'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../config/index.js'
|
||||
import User from '../../src/adapters/localdb/models/users.js'
|
||||
import JsonFiles from '../../src/adapters/json-files.js'
|
||||
|
||||
// Hack to get __dirname back.
|
||||
@@ -33,24 +32,6 @@ async function cleanDb () {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all users in the database. This ensures there is no previous state
|
||||
// to confuse tests.
|
||||
async function deleteAllUsers () {
|
||||
try {
|
||||
// Get all the users in the DB.
|
||||
const users = await User.find({}, '-password')
|
||||
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
// Delete each user.
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const thisUser = users[i]
|
||||
await thisUser.remove()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in test-utils.js/deleteAllUsers()')
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used to create new users.
|
||||
// userObj = {
|
||||
// username,
|
||||
@@ -170,12 +151,56 @@ async function getAdminJWT () {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
// Fetches all users from the database.
|
||||
async function getAllUsers () {
|
||||
try {
|
||||
const adminJWT = await getAdminJWT()
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
return result.data.users
|
||||
} catch (err) {
|
||||
console.error('Error in test/utils.js/getAllUsers()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Deletes all users from the database.
|
||||
async function deleteAllUsers () {
|
||||
try {
|
||||
const allUsers = await getAllUsers()
|
||||
const adminJWT = await getAdminJWT()
|
||||
for (let i = 0; i < allUsers.length; i++) {
|
||||
const user = allUsers[i]
|
||||
// Skip the admin user.
|
||||
if (user.type === 'admin') {
|
||||
continue
|
||||
}
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${user._id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in test/utils.js/deleteAllUsers()', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
export default {
|
||||
cleanDb,
|
||||
createUser,
|
||||
loginTestUser,
|
||||
loginAdminUser,
|
||||
getAdminJWT,
|
||||
deleteAllUsers
|
||||
deleteAllUsers,
|
||||
getAllUsers
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import mongoose from 'mongoose'
|
||||
import config from '../../config/index.js'
|
||||
import User from '../../src/adapters/localdb/models/users.js'
|
||||
|
||||
const EMAIL = 'test@test.com'
|
||||
const PASSWORD = 'pass'
|
||||
const EMAIL = process.env.EMAIL || 'test@test3.com'
|
||||
const PASSWORD = process.env.PASSWORD || 'pass'
|
||||
|
||||
async function addUser () {
|
||||
// Connect to the Mongo Database.
|
||||
@@ -13,8 +14,6 @@ async function addUser () {
|
||||
{ useNewUrlParser: true, useUnifiedTopology: true }
|
||||
)
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
|
||||
const userData = {
|
||||
email: EMAIL,
|
||||
password: PASSWORD
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import mongoose from 'mongoose'
|
||||
import config from '../../config/index.js'
|
||||
import User from '../../src/models/users.js'
|
||||
import User from '../../src/adapters/localdb/models/users.js'
|
||||
|
||||
async function getUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import mongoose from 'mongoose'
|
||||
import config from '../../config/index.js'
|
||||
import User from '../../src/adapters/localdb/models/users.js'
|
||||
|
||||
async function getUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(
|
||||
config.database,
|
||||
{ useNewUrlParser: true, useUnifiedTopology: true }
|
||||
)
|
||||
|
||||
// Find the user by email.
|
||||
const user = await User.findOne({
|
||||
email: 'test@test.com'
|
||||
}, '-password')
|
||||
|
||||
// Update the users password
|
||||
// user.password = 'newpassword'
|
||||
|
||||
// Change the user to an admin
|
||||
// user.type = 'admin'
|
||||
|
||||
// Save the changes to the database.
|
||||
await user.save()
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
getUsers()
|
||||
Reference in New Issue
Block a user