psf-bch-api 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env-local +4 -0
- package/LICENSE.md +8 -0
- package/README.md +8 -0
- package/apidoc.json +9 -0
- package/bin/server.js +183 -0
- package/dev-docs/README.md +4 -0
- package/dev-docs/creation-prompt.md +34 -0
- package/dev-docs/rest2nostr-poxy-api.plan.md +163 -0
- package/dev-docs/test-plan-for-rest2nostr.plan.md +161 -0
- package/dev-docs/unit-test-prompt.md +13 -0
- package/examples/01-create-account.js +67 -0
- package/examples/02-read-posts.js +44 -0
- package/examples/03-write-post.js +55 -0
- package/examples/04-read-alice-posts.js +49 -0
- package/examples/05-get-follow-list.js +53 -0
- package/examples/06-update-follow-list.js +63 -0
- package/examples/07-liking-event.js +59 -0
- package/examples/README.md +90 -0
- package/index.js +11 -0
- package/package.json +37 -0
- package/production/docker/Dockerfile +85 -0
- package/production/docker/cleanup-images.sh +5 -0
- package/production/docker/docker-compose.yml +19 -0
- package/production/docker/start-rest2nostr.sh +3 -0
- package/src/adapters/full-node-rpc.js +133 -0
- package/src/adapters/index.js +217 -0
- package/src/adapters/wlogger.js +79 -0
- package/src/config/env/common.js +64 -0
- package/src/config/env/development.js +7 -0
- package/src/config/env/production.js +7 -0
- package/src/config/index.js +14 -0
- package/src/controllers/index.js +56 -0
- package/src/controllers/rest-api/full-node/blockchain/controller.js +553 -0
- package/src/controllers/rest-api/full-node/blockchain/index.js +66 -0
- package/src/controllers/rest-api/index.js +55 -0
- package/src/controllers/timer-controller.js +72 -0
- package/src/entities/event.js +71 -0
- package/src/use-cases/full-node-blockchain-use-cases.js +134 -0
- package/src/use-cases/index.js +29 -0
- package/test/integration/api/event-integration.js +250 -0
- package/test/integration/api/req-integration.js +173 -0
- package/test/integration/api/subscription-integration.js +198 -0
- package/test/integration/use-cases/manage-subscription-integration.js +163 -0
- package/test/integration/use-cases/publish-event-integration.js +104 -0
- package/test/integration/use-cases/query-events-integration.js +95 -0
- package/test/unit/adapters/full-node-rpc-unit.js +122 -0
- package/test/unit/bin/server-unit.js +63 -0
- package/test/unit/controllers/blockchain-controller-unit.js +215 -0
- package/test/unit/controllers/rest-api-index-unit.js +85 -0
- package/test/unit/entities/event-unit.js +139 -0
- package/test/unit/mocks/controller-mocks.js +98 -0
- package/test/unit/mocks/event-mocks.js +194 -0
- package/test/unit/use-cases/full-node-blockchain-use-cases-unit.js +137 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Integration tests for QueryEventsUseCase with real adapter.
|
|
3
|
+
These tests require a running Nostr relay.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// npm libraries
|
|
7
|
+
import { assert } from 'chai'
|
|
8
|
+
|
|
9
|
+
// Unit under test
|
|
10
|
+
import Adapters from '../../../src/adapters/index.js'
|
|
11
|
+
import QueryEventsUseCase from '../../../src/use-cases/query-events.js'
|
|
12
|
+
|
|
13
|
+
describe('#query-events-integration.js', () => {
|
|
14
|
+
let adapters
|
|
15
|
+
let uut
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
// Initialize adapters (will connect to real relay)
|
|
19
|
+
adapters = new Adapters()
|
|
20
|
+
await adapters.start()
|
|
21
|
+
|
|
22
|
+
uut = new QueryEventsUseCase({ adapters })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
after(async () => {
|
|
26
|
+
// Clean up adapters - disconnect from all relays
|
|
27
|
+
if (adapters && adapters.nostrRelays) {
|
|
28
|
+
await Promise.allSettled(
|
|
29
|
+
adapters.nostrRelays.map(relay => relay.disconnect())
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('#execute()', () => {
|
|
35
|
+
it('should successfully query events', async () => {
|
|
36
|
+
const filters = [{ kinds: [1], limit: 5 }]
|
|
37
|
+
const subscriptionId = 'test-query-' + Date.now()
|
|
38
|
+
|
|
39
|
+
const events = await uut.execute(filters, subscriptionId)
|
|
40
|
+
|
|
41
|
+
// Assert result is an array
|
|
42
|
+
assert.isArray(events)
|
|
43
|
+
|
|
44
|
+
// If events are returned, verify structure
|
|
45
|
+
if (events.length > 0) {
|
|
46
|
+
assert.property(events[0], 'id')
|
|
47
|
+
assert.property(events[0], 'pubkey')
|
|
48
|
+
assert.property(events[0], 'created_at')
|
|
49
|
+
assert.property(events[0], 'kind')
|
|
50
|
+
assert.property(events[0], 'content')
|
|
51
|
+
assert.equal(events[0].kind, 1)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should handle empty results', async () => {
|
|
56
|
+
// Query for events that likely don't exist
|
|
57
|
+
const filters = [{ kinds: [99999], limit: 1 }]
|
|
58
|
+
const subscriptionId = 'test-empty-' + Date.now()
|
|
59
|
+
|
|
60
|
+
const events = await uut.execute(filters, subscriptionId)
|
|
61
|
+
|
|
62
|
+
// Should return empty array, not throw
|
|
63
|
+
assert.isArray(events)
|
|
64
|
+
assert.equal(events.length, 0)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should handle multiple filters', async function () {
|
|
68
|
+
// Increase timeout for this test - needs to be longer than use case timeout (30s)
|
|
69
|
+
this.timeout(35000)
|
|
70
|
+
|
|
71
|
+
const filters = [
|
|
72
|
+
{ kinds: [1], limit: 2 },
|
|
73
|
+
{ kinds: [3], limit: 2 }
|
|
74
|
+
]
|
|
75
|
+
const subscriptionId = 'test-multi-' + Date.now()
|
|
76
|
+
|
|
77
|
+
const events = await uut.execute(filters, subscriptionId)
|
|
78
|
+
|
|
79
|
+
// Should return array (may be empty)
|
|
80
|
+
assert.isArray(events)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should timeout if EOSE not received', async () => {
|
|
84
|
+
// This test may take up to 30 seconds
|
|
85
|
+
// Use a filter that might not return EOSE quickly
|
|
86
|
+
const filters = [{ kinds: [1] }] // No limit, might timeout
|
|
87
|
+
const subscriptionId = 'test-timeout-' + Date.now()
|
|
88
|
+
|
|
89
|
+
// Should eventually return (even if empty)
|
|
90
|
+
const events = await uut.execute(filters, subscriptionId)
|
|
91
|
+
|
|
92
|
+
assert.isArray(events)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for FullNodeRPCAdapter.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
import axios from 'axios'
|
|
8
|
+
|
|
9
|
+
import FullNodeRPCAdapter from '../../../src/adapters/full-node-rpc.js'
|
|
10
|
+
|
|
11
|
+
describe('#full-node-rpc.js', () => {
|
|
12
|
+
let sandbox
|
|
13
|
+
let axiosCreateStub
|
|
14
|
+
let mockAxiosInstance
|
|
15
|
+
|
|
16
|
+
const baseConfig = {
|
|
17
|
+
fullNode: {
|
|
18
|
+
rpcBaseUrl: 'http://127.0.0.1:8332',
|
|
19
|
+
rpcUsername: 'user',
|
|
20
|
+
rpcPassword: 'pass',
|
|
21
|
+
rpcTimeoutMs: 1000,
|
|
22
|
+
rpcRequestIdPrefix: 'test'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
sandbox = sinon.createSandbox()
|
|
28
|
+
mockAxiosInstance = {
|
|
29
|
+
post: sandbox.stub()
|
|
30
|
+
}
|
|
31
|
+
axiosCreateStub = sandbox.stub(axios, 'create').returns(mockAxiosInstance)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
sandbox.restore()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('#constructor()', () => {
|
|
39
|
+
it('should throw if full node config is missing', () => {
|
|
40
|
+
assert.throws(() => {
|
|
41
|
+
// eslint-disable-next-line no-new
|
|
42
|
+
new FullNodeRPCAdapter({ config: {} })
|
|
43
|
+
}, /Full node RPC configuration is required/)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should create axios client with provided configuration', () => {
|
|
47
|
+
// eslint-disable-next-line no-new
|
|
48
|
+
new FullNodeRPCAdapter({ config: baseConfig })
|
|
49
|
+
|
|
50
|
+
assert.isTrue(axiosCreateStub.calledOnce)
|
|
51
|
+
const options = axiosCreateStub.getCall(0).args[0]
|
|
52
|
+
assert.equal(options.baseURL, baseConfig.fullNode.rpcBaseUrl)
|
|
53
|
+
assert.equal(options.timeout, baseConfig.fullNode.rpcTimeoutMs)
|
|
54
|
+
assert.deepEqual(options.auth, {
|
|
55
|
+
username: baseConfig.fullNode.rpcUsername,
|
|
56
|
+
password: baseConfig.fullNode.rpcPassword
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('#call()', () => {
|
|
62
|
+
it('should call RPC method and return result', async () => {
|
|
63
|
+
mockAxiosInstance.post.resolves({ data: { result: 'hash' } })
|
|
64
|
+
const uut = new FullNodeRPCAdapter({ config: baseConfig })
|
|
65
|
+
|
|
66
|
+
const result = await uut.call('getbestblockhash', [])
|
|
67
|
+
|
|
68
|
+
assert.equal(result, 'hash')
|
|
69
|
+
assert.isTrue(mockAxiosInstance.post.calledOnce)
|
|
70
|
+
const [, payload] = mockAxiosInstance.post.getCall(0).args
|
|
71
|
+
assert.deepEqual(payload, {
|
|
72
|
+
jsonrpc: '1.0',
|
|
73
|
+
id: 'test-getbestblockhash',
|
|
74
|
+
method: 'getbestblockhash',
|
|
75
|
+
params: []
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should use custom request id when provided', async () => {
|
|
80
|
+
mockAxiosInstance.post.resolves({ data: { result: 123 } })
|
|
81
|
+
const uut = new FullNodeRPCAdapter({ config: baseConfig })
|
|
82
|
+
|
|
83
|
+
await uut.call('getblockcount', [], 'custom-id')
|
|
84
|
+
|
|
85
|
+
const [, payload] = mockAxiosInstance.post.getCall(0).args
|
|
86
|
+
assert.equal(payload.id, 'custom-id')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should throw formatted error when RPC returns error', async () => {
|
|
90
|
+
mockAxiosInstance.post.resolves({
|
|
91
|
+
data: {
|
|
92
|
+
error: { message: 'RPC error' }
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
const uut = new FullNodeRPCAdapter({ config: baseConfig })
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await uut.call('failing', [])
|
|
99
|
+
assert.fail('Unexpected success')
|
|
100
|
+
} catch (err) {
|
|
101
|
+
assert.equal(err.message, 'RPC error')
|
|
102
|
+
assert.equal(err.status, 400)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should translate network errors into 503 status', async () => {
|
|
107
|
+
mockAxiosInstance.post.rejects(new Error('ENOTFOUND fullnode'))
|
|
108
|
+
const uut = new FullNodeRPCAdapter({ config: baseConfig })
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await uut.call('getblockcount', [])
|
|
112
|
+
assert.fail('Unexpected success')
|
|
113
|
+
} catch (err) {
|
|
114
|
+
assert.equal(
|
|
115
|
+
err.message,
|
|
116
|
+
'Network error: Could not communicate with full node or other external service.'
|
|
117
|
+
)
|
|
118
|
+
assert.equal(err.status, 503)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for Server class.
|
|
3
|
+
Note: Full server testing requires integration tests due to ES module limitations.
|
|
4
|
+
These tests focus on testable logic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// npm libraries
|
|
8
|
+
import { assert } from 'chai'
|
|
9
|
+
import sinon from 'sinon'
|
|
10
|
+
|
|
11
|
+
// Unit under test
|
|
12
|
+
import Server from '../../../bin/server.js'
|
|
13
|
+
|
|
14
|
+
describe('#server.js', () => {
|
|
15
|
+
let sandbox
|
|
16
|
+
let uut
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
sandbox = sinon.createSandbox()
|
|
20
|
+
uut = new Server()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
sandbox.restore()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('#startServer()', () => {
|
|
28
|
+
// Note: Full server startup testing requires integration tests
|
|
29
|
+
// due to ES module import limitations with Express and Controllers
|
|
30
|
+
it('should have startServer method', () => {
|
|
31
|
+
assert.isFunction(uut.startServer)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should have controllers property', () => {
|
|
35
|
+
assert.property(uut, 'controllers')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should have config property', () => {
|
|
39
|
+
assert.property(uut, 'config')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('#sleep()', () => {
|
|
44
|
+
it('should sleep for specified milliseconds', async () => {
|
|
45
|
+
const start = Date.now()
|
|
46
|
+
await uut.sleep(50)
|
|
47
|
+
const end = Date.now()
|
|
48
|
+
|
|
49
|
+
// Should have slept at least 50ms (allowing some margin)
|
|
50
|
+
assert.isAtLeast(end - start, 40)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('#constructor()', () => {
|
|
55
|
+
it('should initialize with controllers and config', () => {
|
|
56
|
+
const server = new Server()
|
|
57
|
+
|
|
58
|
+
assert.property(server, 'controllers')
|
|
59
|
+
assert.property(server, 'config')
|
|
60
|
+
assert.property(server, 'process')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for BlockchainRESTController.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
|
|
8
|
+
import BlockchainRESTController from '../../../src/controllers/rest-api/full-node/blockchain/controller.js'
|
|
9
|
+
import {
|
|
10
|
+
createMockRequest,
|
|
11
|
+
createMockResponse
|
|
12
|
+
} from '../mocks/controller-mocks.js'
|
|
13
|
+
|
|
14
|
+
describe('#blockchain-controller.js', () => {
|
|
15
|
+
let sandbox
|
|
16
|
+
let mockUseCases
|
|
17
|
+
let mockAdapters
|
|
18
|
+
let uut
|
|
19
|
+
|
|
20
|
+
const createBlockchainUseCaseStubs = () => ({
|
|
21
|
+
getBestBlockHash: sandbox.stub().resolves('hash'),
|
|
22
|
+
getBlockchainInfo: sandbox.stub().resolves({}),
|
|
23
|
+
getBlockCount: sandbox.stub().resolves(123),
|
|
24
|
+
getBlockHeader: sandbox.stub().resolves({ header: true }),
|
|
25
|
+
getBlockHeaders: sandbox.stub().resolves(['header']),
|
|
26
|
+
getChainTips: sandbox.stub().resolves(['tip']),
|
|
27
|
+
getDifficulty: sandbox.stub().resolves(1),
|
|
28
|
+
getMempoolEntry: sandbox.stub().resolves({}),
|
|
29
|
+
getMempoolEntries: sandbox.stub().resolves([]),
|
|
30
|
+
getMempoolAncestors: sandbox.stub().resolves([]),
|
|
31
|
+
getMempoolInfo: sandbox.stub().resolves({ size: 1 }),
|
|
32
|
+
getRawMempool: sandbox.stub().resolves(['tx']),
|
|
33
|
+
getTxOut: sandbox.stub().resolves({ value: 1 }),
|
|
34
|
+
getTxOutProof: sandbox.stub().resolves('proof'),
|
|
35
|
+
getTxOutProofs: sandbox.stub().resolves(['proof']),
|
|
36
|
+
verifyTxOutProof: sandbox.stub().resolves(['txid']),
|
|
37
|
+
verifyTxOutProofs: sandbox.stub().resolves([['txid']]),
|
|
38
|
+
getBlock: sandbox.stub().resolves({ hash: 'abc' }),
|
|
39
|
+
getBlockHash: sandbox.stub().resolves('blockhash')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
sandbox = sinon.createSandbox()
|
|
44
|
+
mockAdapters = {
|
|
45
|
+
fullNode: {
|
|
46
|
+
validateArraySize: sandbox.stub().returns(true)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
mockUseCases = {
|
|
50
|
+
blockchain: createBlockchainUseCaseStubs()
|
|
51
|
+
}
|
|
52
|
+
uut = new BlockchainRESTController({
|
|
53
|
+
adapters: mockAdapters,
|
|
54
|
+
useCases: mockUseCases
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
sandbox.restore()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('#constructor()', () => {
|
|
63
|
+
it('should require adapters', () => {
|
|
64
|
+
assert.throws(() => {
|
|
65
|
+
// eslint-disable-next-line no-new
|
|
66
|
+
new BlockchainRESTController({ useCases: mockUseCases })
|
|
67
|
+
}, /Adapters library required/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should require blockchain use cases', () => {
|
|
71
|
+
assert.throws(() => {
|
|
72
|
+
// eslint-disable-next-line no-new
|
|
73
|
+
new BlockchainRESTController({ adapters: mockAdapters, useCases: {} })
|
|
74
|
+
}, /Blockchain use cases required/)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('#root()', () => {
|
|
79
|
+
it('should return service status', async () => {
|
|
80
|
+
const req = createMockRequest()
|
|
81
|
+
const res = createMockResponse()
|
|
82
|
+
|
|
83
|
+
await uut.root(req, res)
|
|
84
|
+
|
|
85
|
+
assert.equal(res.statusValue, 200)
|
|
86
|
+
assert.deepEqual(res.jsonData, { status: 'blockchain' })
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('#getBestBlockHash()', () => {
|
|
91
|
+
it('should return hash on success', async () => {
|
|
92
|
+
const req = createMockRequest()
|
|
93
|
+
const res = createMockResponse()
|
|
94
|
+
|
|
95
|
+
await uut.getBestBlockHash(req, res)
|
|
96
|
+
|
|
97
|
+
assert.equal(res.statusValue, 200)
|
|
98
|
+
assert.equal(res.jsonData, 'hash')
|
|
99
|
+
assert.isTrue(mockUseCases.blockchain.getBestBlockHash.calledOnce)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should handle errors via handleError()', async () => {
|
|
103
|
+
const error = new Error('failure')
|
|
104
|
+
error.status = 422
|
|
105
|
+
mockUseCases.blockchain.getBestBlockHash.rejects(error)
|
|
106
|
+
const req = createMockRequest()
|
|
107
|
+
const res = createMockResponse()
|
|
108
|
+
|
|
109
|
+
await uut.getBestBlockHash(req, res)
|
|
110
|
+
|
|
111
|
+
assert.equal(res.statusValue, 422)
|
|
112
|
+
assert.deepEqual(res.jsonData, { error: 'failure' })
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('#getBlockHeaderSingle()', () => {
|
|
117
|
+
it('should return 400 if hash is missing', async () => {
|
|
118
|
+
const req = createMockRequest()
|
|
119
|
+
const res = createMockResponse()
|
|
120
|
+
|
|
121
|
+
await uut.getBlockHeaderSingle(req, res)
|
|
122
|
+
|
|
123
|
+
assert.equal(res.statusValue, 400)
|
|
124
|
+
assert.property(res.jsonData, 'error')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should call use case with verbose flag', async () => {
|
|
128
|
+
const hash = 'a'.repeat(64)
|
|
129
|
+
const req = createMockRequest({
|
|
130
|
+
params: { hash },
|
|
131
|
+
query: { verbose: 'true' }
|
|
132
|
+
})
|
|
133
|
+
const res = createMockResponse()
|
|
134
|
+
|
|
135
|
+
await uut.getBlockHeaderSingle(req, res)
|
|
136
|
+
|
|
137
|
+
assert.equal(res.statusValue, 200)
|
|
138
|
+
assert.isTrue(
|
|
139
|
+
mockUseCases.blockchain.getBlockHeader.calledOnceWithExactly({
|
|
140
|
+
hash,
|
|
141
|
+
verbose: true
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('#getBlockHeaderBulk()', () => {
|
|
148
|
+
it('should return error if hashes is not array', async () => {
|
|
149
|
+
const req = createMockRequest({
|
|
150
|
+
body: { hashes: 'not-an-array' },
|
|
151
|
+
locals: {}
|
|
152
|
+
})
|
|
153
|
+
const res = createMockResponse()
|
|
154
|
+
|
|
155
|
+
await uut.getBlockHeaderBulk(req, res)
|
|
156
|
+
|
|
157
|
+
assert.equal(res.statusValue, 400)
|
|
158
|
+
assert.include(res.jsonData.error, 'hashes needs to be an array')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should validate array size and call use case', async () => {
|
|
162
|
+
const hash = 'a'.repeat(64)
|
|
163
|
+
const req = createMockRequest({
|
|
164
|
+
body: { hashes: [hash], verbose: true },
|
|
165
|
+
locals: { proLimit: false }
|
|
166
|
+
})
|
|
167
|
+
const res = createMockResponse()
|
|
168
|
+
mockUseCases.blockchain.getBlockHeaders.resolves(['result'])
|
|
169
|
+
|
|
170
|
+
await uut.getBlockHeaderBulk(req, res)
|
|
171
|
+
|
|
172
|
+
assert.equal(res.statusValue, 200)
|
|
173
|
+
assert.deepEqual(res.jsonData, ['result'])
|
|
174
|
+
assert.isTrue(
|
|
175
|
+
mockAdapters.fullNode.validateArraySize.calledOnceWithExactly(1, { isProUser: false })
|
|
176
|
+
)
|
|
177
|
+
assert.isTrue(
|
|
178
|
+
mockUseCases.blockchain.getBlockHeaders.calledOnceWithExactly({
|
|
179
|
+
hashes: [hash],
|
|
180
|
+
verbose: true
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should return error if array size invalid', async () => {
|
|
186
|
+
mockAdapters.fullNode.validateArraySize.returns(false)
|
|
187
|
+
const req = createMockRequest({
|
|
188
|
+
body: { hashes: ['a'.repeat(64)] },
|
|
189
|
+
locals: {}
|
|
190
|
+
})
|
|
191
|
+
const res = createMockResponse()
|
|
192
|
+
|
|
193
|
+
await uut.getBlockHeaderBulk(req, res)
|
|
194
|
+
|
|
195
|
+
assert.equal(res.statusValue, 400)
|
|
196
|
+
assert.equal(res.jsonData.error, 'Array too large.')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('#verifyTxOutProofBulk()', () => {
|
|
201
|
+
it('should flatten proof responses', async () => {
|
|
202
|
+
mockUseCases.blockchain.verifyTxOutProofs.resolves([['txid-a'], ['txid-b']])
|
|
203
|
+
const req = createMockRequest({
|
|
204
|
+
body: { proofs: ['proof-a', 'proof-b'] },
|
|
205
|
+
locals: {}
|
|
206
|
+
})
|
|
207
|
+
const res = createMockResponse()
|
|
208
|
+
|
|
209
|
+
await uut.verifyTxOutProofBulk(req, res)
|
|
210
|
+
|
|
211
|
+
assert.equal(res.statusValue, 200)
|
|
212
|
+
assert.deepEqual(res.jsonData, ['txid-a', 'txid-b'])
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for RESTControllers index.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
|
|
8
|
+
import RESTControllers from '../../../src/controllers/rest-api/index.js'
|
|
9
|
+
import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockchain/index.js'
|
|
10
|
+
|
|
11
|
+
describe('#controllers/rest-api/index.js', () => {
|
|
12
|
+
let sandbox
|
|
13
|
+
let mockAdapters
|
|
14
|
+
let mockUseCases
|
|
15
|
+
|
|
16
|
+
const createBlockchainUseCaseStubs = () => ({
|
|
17
|
+
getBestBlockHash: () => {},
|
|
18
|
+
getBlockchainInfo: () => {},
|
|
19
|
+
getBlockCount: () => {},
|
|
20
|
+
getBlockHeader: () => {},
|
|
21
|
+
getBlockHeaders: () => {},
|
|
22
|
+
getChainTips: () => {},
|
|
23
|
+
getDifficulty: () => {},
|
|
24
|
+
getMempoolEntry: () => {},
|
|
25
|
+
getMempoolEntries: () => {},
|
|
26
|
+
getMempoolAncestors: () => {},
|
|
27
|
+
getMempoolInfo: () => {},
|
|
28
|
+
getRawMempool: () => {},
|
|
29
|
+
getTxOut: () => {},
|
|
30
|
+
getTxOutProof: () => {},
|
|
31
|
+
getTxOutProofs: () => {},
|
|
32
|
+
verifyTxOutProof: () => {},
|
|
33
|
+
verifyTxOutProofs: () => {},
|
|
34
|
+
getBlock: () => {},
|
|
35
|
+
getBlockHash: () => {}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
sandbox = sinon.createSandbox()
|
|
40
|
+
mockAdapters = {
|
|
41
|
+
fullNode: {
|
|
42
|
+
validateArraySize: sandbox.stub().returns(true)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
mockUseCases = {
|
|
46
|
+
blockchain: createBlockchainUseCaseStubs()
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
sandbox.restore()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('#constructor()', () => {
|
|
55
|
+
it('should require adapters instance', () => {
|
|
56
|
+
assert.throws(() => {
|
|
57
|
+
// eslint-disable-next-line no-new
|
|
58
|
+
new RESTControllers({ useCases: mockUseCases })
|
|
59
|
+
}, /Adapters library required/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should require useCases instance', () => {
|
|
63
|
+
assert.throws(() => {
|
|
64
|
+
// eslint-disable-next-line no-new
|
|
65
|
+
new RESTControllers({ adapters: mockAdapters })
|
|
66
|
+
}, /Use Cases library required/)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('#attachRESTControllers()', () => {
|
|
71
|
+
it('should instantiate blockchain router and attach to app', () => {
|
|
72
|
+
const attachStub = sandbox.stub(BlockchainRouter.prototype, 'attach')
|
|
73
|
+
const restControllers = new RESTControllers({
|
|
74
|
+
adapters: mockAdapters,
|
|
75
|
+
useCases: mockUseCases
|
|
76
|
+
})
|
|
77
|
+
const app = {}
|
|
78
|
+
|
|
79
|
+
restControllers.attachRESTControllers(app)
|
|
80
|
+
|
|
81
|
+
assert.isTrue(attachStub.calledOnce)
|
|
82
|
+
assert.equal(attachStub.getCall(0).args[0], app)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|