mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2290-New-Design
This commit is contained in:
commit
e930aa9d4f
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -527,7 +527,7 @@ jobs:
|
|||||||
report_name: Coverage Backend
|
report_name: Coverage Backend
|
||||||
type: lcov
|
type: lcov
|
||||||
result_path: ./backend/coverage/lcov.info
|
result_path: ./backend/coverage/lcov.info
|
||||||
min_coverage: 74
|
min_coverage: 76
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
CONFIG_VERSION=v13.2022-12-20
|
CONFIG_VERSION=v14.2022-12-22
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
PORT=4000
|
PORT=4000
|
||||||
@ -67,3 +67,4 @@ EVENT_PROTOCOL_DISABLED=false
|
|||||||
# on an hash created from this topic
|
# on an hash created from this topic
|
||||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||||
|
# FEDERATION_COMMUNITY_URL=http://localhost:4000/api
|
||||||
|
|||||||
@ -60,3 +60,4 @@ EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
|
|||||||
# Federation
|
# Federation
|
||||||
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
||||||
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
||||||
|
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hyperswarm/dht": "^6.2.0",
|
"@hyperswarm/dht": "^6.2.0",
|
||||||
"apollo-server-express": "^2.25.2",
|
"apollo-server-express": "^2.25.2",
|
||||||
|
"await-semaphore": "^0.1.3",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@ -10,14 +10,14 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0057-clear_old_password_junk',
|
DB_VERSION: '0058-add_communities_table',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||||
CONFIG_VERSION: {
|
CONFIG_VERSION: {
|
||||||
DEFAULT: 'DEFAULT',
|
DEFAULT: 'DEFAULT',
|
||||||
EXPECTED: 'v13.2022-12-20',
|
EXPECTED: 'v14.2022-11-22',
|
||||||
CURRENT: '',
|
CURRENT: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -120,6 +120,12 @@ if (
|
|||||||
const federation = {
|
const federation = {
|
||||||
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
|
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
|
||||||
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
|
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
|
||||||
|
FEDERATION_COMMUNITY_URL:
|
||||||
|
process.env.FEDERATION_COMMUNITY_URL === undefined
|
||||||
|
? null
|
||||||
|
: process.env.FEDERATION_COMMUNITY_URL.endsWith('/')
|
||||||
|
? process.env.FEDERATION_COMMUNITY_URL
|
||||||
|
: process.env.FEDERATION_COMMUNITY_URL + '/',
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
|
|||||||
798
backend/src/federation/index.test.ts
Normal file
798
backend/src/federation/index.test.ts
Normal file
@ -0,0 +1,798 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import { startDHT } from './index'
|
||||||
|
import DHT from '@hyperswarm/dht'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { logger } from '@test/testSetup'
|
||||||
|
import { Community as DbCommunity } from '@entity/Community'
|
||||||
|
import { testEnvironment, cleanDB } from '@test/helpers'
|
||||||
|
|
||||||
|
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
|
||||||
|
|
||||||
|
jest.mock('@hyperswarm/dht')
|
||||||
|
|
||||||
|
const TEST_TOPIC = 'gradido_test_topic'
|
||||||
|
|
||||||
|
const keyPairMock = {
|
||||||
|
publicKey: Buffer.from('publicKey'),
|
||||||
|
secretKey: Buffer.from('secretKey'),
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverListenSpy = jest.fn()
|
||||||
|
|
||||||
|
const serverEventMocks: { [key: string]: any } = {}
|
||||||
|
|
||||||
|
const serverOnMock = jest.fn().mockImplementation((key: string, callback) => {
|
||||||
|
serverEventMocks[key] = callback
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeCreateServerMock = jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
on: serverOnMock,
|
||||||
|
listen: serverListenSpy,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeAnnounceMock = jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
finished: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const lookupResultMock = {
|
||||||
|
token: Buffer.from(TEST_TOPIC),
|
||||||
|
from: {
|
||||||
|
id: Buffer.from('somone'),
|
||||||
|
host: '188.95.53.5',
|
||||||
|
port: 63561,
|
||||||
|
},
|
||||||
|
to: { id: null, host: '83.53.31.27', port: 55723 },
|
||||||
|
peers: [
|
||||||
|
{
|
||||||
|
publicKey: Buffer.from('some-public-key'),
|
||||||
|
relayAddresses: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeLookupMock = jest.fn().mockResolvedValue([lookupResultMock])
|
||||||
|
|
||||||
|
const socketEventMocks: { [key: string]: any } = {}
|
||||||
|
|
||||||
|
const socketOnMock = jest.fn().mockImplementation((key: string, callback) => {
|
||||||
|
socketEventMocks[key] = callback
|
||||||
|
})
|
||||||
|
|
||||||
|
const socketWriteMock = jest.fn()
|
||||||
|
|
||||||
|
const nodeConnectMock = jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
on: socketOnMock,
|
||||||
|
once: socketOnMock,
|
||||||
|
write: socketWriteMock,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
DHT.hash.mockImplementation(() => {
|
||||||
|
return Buffer.from(TEST_TOPIC)
|
||||||
|
})
|
||||||
|
|
||||||
|
DHT.keyPair.mockImplementation(() => {
|
||||||
|
return keyPairMock
|
||||||
|
})
|
||||||
|
|
||||||
|
DHT.mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
createServer: nodeCreateServerMock,
|
||||||
|
announce: nodeAnnounceMock,
|
||||||
|
lookup: nodeLookupMock,
|
||||||
|
connect: nodeConnectMock,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let con: any
|
||||||
|
let testEnv: any
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testEnv = await testEnvironment(logger)
|
||||||
|
con = testEnv.con
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
await con.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('federation', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('call startDHT', () => {
|
||||||
|
const hashSpy = jest.spyOn(DHT, 'hash')
|
||||||
|
const keyPairSpy = jest.spyOn(DHT, 'keyPair')
|
||||||
|
beforeEach(async () => {
|
||||||
|
DHT.mockClear()
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await startDHT(TEST_TOPIC)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls DHT.hash', () => {
|
||||||
|
expect(hashSpy).toBeCalledWith(Buffer.from(TEST_TOPIC))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates a key pair', () => {
|
||||||
|
expect(keyPairSpy).toBeCalledWith(expect.any(Buffer))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes a new DHT object', () => {
|
||||||
|
expect(DHT).toBeCalledWith({ keyPair: keyPairMock })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DHT node', () => {
|
||||||
|
it('creates a server', () => {
|
||||||
|
expect(nodeCreateServerMock).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('listens on the server', () => {
|
||||||
|
expect(serverListenSpy).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('timers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.runOnlyPendingTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('announces on topic', () => {
|
||||||
|
expect(nodeAnnounceMock).toBeCalledWith(Buffer.from(TEST_TOPIC), keyPairMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('looks up on topic', () => {
|
||||||
|
expect(nodeLookupMock).toBeCalledWith(Buffer.from(TEST_TOPIC))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('server connection event', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
serverEventMocks.connection({
|
||||||
|
remotePublicKey: Buffer.from('another-public-key'),
|
||||||
|
on: socketOnMock,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be triggered', () => {
|
||||||
|
expect(socketOnMock).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('socket events', () => {
|
||||||
|
describe('on data', () => {
|
||||||
|
describe('with receiving simply a string', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
socketEventMocks.data(Buffer.from('no-json string'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith('data: no-json string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs an error of unexpected data format and structure', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Error on receiving data from socket:',
|
||||||
|
new SyntaxError('Unexpected token o in JSON at position 1'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving array of strings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const strArray: string[] = ['invalid type test', 'api', 'url']
|
||||||
|
socketEventMocks.data(Buffer.from(strArray.toString()))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith('data: invalid type test,api,url')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs an error of unexpected data format and structure', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Error on receiving data from socket:',
|
||||||
|
new SyntaxError('Unexpected token i in JSON at position 0'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving array of string-arrays', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const strArray: string[][] = [
|
||||||
|
[`api`, `url`, `invalid type in array test`],
|
||||||
|
[`wrong`, `api`, `url`],
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(strArray.toString()))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(
|
||||||
|
'data: api,url,invalid type in array test,wrong,api,url',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs an error of unexpected data format and structure', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Error on receiving data from socket:',
|
||||||
|
new SyntaxError('Unexpected token a in JSON at position 0'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving JSON-Array with too much entries', () => {
|
||||||
|
let jsonArray: { api: string; url: string }[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 'v1_0', url: 'too much versions at the same time test' },
|
||||||
|
{ api: 'v1_0', url: 'url2' },
|
||||||
|
{ api: 'v1_0', url: 'url3' },
|
||||||
|
{ api: 'v1_0', url: 'url4' },
|
||||||
|
{ api: 'v1_0', url: 'url5' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(
|
||||||
|
'data: [{"api":"v1_0","url":"too much versions at the same time test"},{"api":"v1_0","url":"url2"},{"api":"v1_0","url":"url3"},{"api":"v1_0","url":"url4"},{"api":"v1_0","url":"url5"}]',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of too much apiVersion-Definitions', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
|
||||||
|
jsonArray,
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving wrong but tolerated property data', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
let result: DbCommunity[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
wrong: 'wrong but tolerated property test',
|
||||||
|
api: 'v1_0',
|
||||||
|
url: 'url1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'v2_0',
|
||||||
|
url: 'url2',
|
||||||
|
wrong: 'wrong but tolerated property test',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
result = await DbCommunity.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has two Communty entries in database', () => {
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has an entry for api version v1_0', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'v1_0',
|
||||||
|
endPoint: 'url1',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has an entry for api version v2_0', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'v2_0',
|
||||||
|
endPoint: 'url2',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but missing api property', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ test1: 'missing api proterty test', url: 'any url definition as string' },
|
||||||
|
{ api: 'some api', test2: 'missing url property test' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but missing url property', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 'some api', test2: 'missing url property test' },
|
||||||
|
{ test1: 'missing api proterty test', url: 'any url definition as string' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but wrong type of api property', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 1, url: 'wrong property type tests' },
|
||||||
|
{ api: 'urltyptest', url: 2 },
|
||||||
|
{ api: 1, url: 2 },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but wrong type of url property', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 'urltyptest', url: 2 },
|
||||||
|
{ api: 1, url: 'wrong property type tests' },
|
||||||
|
{ api: 1, url: 2 },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but wrong type of both properties', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 1, url: 2 },
|
||||||
|
{ api: 'urltyptest', url: 2 },
|
||||||
|
{ api: 1, url: 'wrong property type tests' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but too long api string', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 'toolong api', url: 'some valid url' },
|
||||||
|
{
|
||||||
|
api: 'valid api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'toolong api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received apiVersion with content longer than max length: ${JSON.stringify(
|
||||||
|
jsonArray[0],
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but too long url string', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{ api: 'toolong api', url: 'some valid url' },
|
||||||
|
{
|
||||||
|
api: 'toolong api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received apiVersion with content longer than max length: ${JSON.stringify(
|
||||||
|
jsonArray[0],
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but both properties with too long strings', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'toolong api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{ api: 'toolong api', url: 'some valid url' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data of exact max allowed properties length', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
let result: DbCommunity[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'valid api',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{ api: 'toolong api', url: 'some valid url' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
result = await DbCommunity.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has one Communty entry in database', () => {
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data of exact max allowed buffer length', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
let result: DbCommunity[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'valid api1',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api2',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api3',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api4',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
result = await DbCommunity.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has five Communty entries in database', () => {
|
||||||
|
expect(result).toHaveLength(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry 'valid api1' with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api1',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry 'valid api2' with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api2',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry 'valid api3' with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api3',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry 'valid api4' with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api4',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data longer than max allowed buffer length', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'Xvalid api1',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api2',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api3',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api4',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received more than max allowed length of data buffer: ${
|
||||||
|
JSON.stringify(jsonArray).length
|
||||||
|
} against 1141 max allowed`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with proper data', () => {
|
||||||
|
let result: DbCommunity[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await socketEventMocks.data(
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
api: 'v1_0',
|
||||||
|
url: 'http://localhost:4000/api/v1_0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'v2_0',
|
||||||
|
url: 'http://localhost:4000/api/v2_0',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await DbCommunity.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has two Communty entries in database', () => {
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has an entry for api version v1_0', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'v1_0',
|
||||||
|
endPoint: 'http://localhost:4000/api/v1_0',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has an entry for api version v2_0', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'v2_0',
|
||||||
|
endPoint: 'http://localhost:4000/api/v2_0',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('on open', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
socketEventMocks.open()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls socket write with own api versions', () => {
|
||||||
|
expect(socketWriteMock).toBeCalledWith(
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
api: 'v1_0',
|
||||||
|
url: 'http://localhost:4000/api/v1_0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'v1_1',
|
||||||
|
url: 'http://localhost:4000/api/v1_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'v2_0',
|
||||||
|
url: 'http://localhost:4000/api/v2_0',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,14 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
import DHT from '@hyperswarm/dht'
|
import DHT from '@hyperswarm/dht'
|
||||||
// import { Connection } from '@dbTools/typeorm'
|
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
|
import { Community as DbCommunity } from '@entity/Community'
|
||||||
function between(min: number, max: number) {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1) + min)
|
|
||||||
}
|
|
||||||
|
|
||||||
const KEY_SECRET_SEEDBYTES = 32
|
const KEY_SECRET_SEEDBYTES = 32
|
||||||
const getSeed = (): Buffer | null =>
|
const getSeed = (): Buffer | null =>
|
||||||
@ -18,37 +13,107 @@ const POLLTIME = 20000
|
|||||||
const SUCCESSTIME = 120000
|
const SUCCESSTIME = 120000
|
||||||
const ERRORTIME = 240000
|
const ERRORTIME = 240000
|
||||||
const ANNOUNCETIME = 30000
|
const ANNOUNCETIME = 30000
|
||||||
const nodeRand = between(1, 99)
|
|
||||||
const nodeURL = `https://test${nodeRand}.org`
|
enum ApiVersionType {
|
||||||
const nodeAPI = {
|
V1_0 = 'v1_0',
|
||||||
API_1_00: `${nodeURL}/api/1_00/`,
|
V1_1 = 'v1_1',
|
||||||
API_1_01: `${nodeURL}/api/1_01/`,
|
V2_0 = 'v2_0',
|
||||||
API_2_00: `${nodeURL}/graphql/2_00/`,
|
}
|
||||||
|
type CommunityApi = {
|
||||||
|
api: string
|
||||||
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startDHT = async (
|
export const startDHT = async (topic: string): Promise<void> => {
|
||||||
// connection: Connection,
|
|
||||||
topic: string,
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
const TOPIC = DHT.hash(Buffer.from(topic))
|
const TOPIC = DHT.hash(Buffer.from(topic))
|
||||||
const keyPair = DHT.keyPair(getSeed())
|
const keyPair = DHT.keyPair(getSeed())
|
||||||
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
|
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
|
||||||
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
|
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
|
||||||
|
|
||||||
|
const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) {
|
||||||
|
const comApi: CommunityApi = {
|
||||||
|
api: apiEnum,
|
||||||
|
url: CONFIG.FEDERATION_COMMUNITY_URL + apiEnum,
|
||||||
|
}
|
||||||
|
return comApi
|
||||||
|
})
|
||||||
|
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
|
||||||
|
|
||||||
const node = new DHT({ keyPair })
|
const node = new DHT({ keyPair })
|
||||||
|
|
||||||
const server = node.createServer()
|
const server = node.createServer()
|
||||||
|
|
||||||
server.on('connection', function (socket: any) {
|
server.on('connection', function (socket: any) {
|
||||||
// noiseSocket is E2E between you and the other peer
|
logger.info(`server on... with Remote public key: ${socket.remotePublicKey.toString('hex')}`)
|
||||||
// pipe it somewhere like any duplex stream
|
|
||||||
logger.info(`Remote public key: ${socket.remotePublicKey.toString('hex')}`)
|
|
||||||
// console.log("Local public key", noiseSocket.publicKey.toString("hex")); // same as keyPair.publicKey
|
|
||||||
|
|
||||||
socket.on('data', (data: Buffer) => logger.info(`data: ${data.toString('ascii')}`))
|
socket.on('data', async (data: Buffer) => {
|
||||||
|
try {
|
||||||
|
if (data.length > 1141) {
|
||||||
|
logger.warn(
|
||||||
|
`received more than max allowed length of data buffer: ${data.length} against 1141 max allowed`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.info(`data: ${data.toString('ascii')}`)
|
||||||
|
const recApiVersions: CommunityApi[] = JSON.parse(data.toString('ascii'))
|
||||||
|
|
||||||
// process.stdin.pipe(noiseSocket).pipe(process.stdout);
|
// TODO better to introduce the validation by https://github.com/typestack/class-validato
|
||||||
|
if (recApiVersions && Array.isArray(recApiVersions) && recApiVersions.length < 5) {
|
||||||
|
for (const recApiVersion of recApiVersions) {
|
||||||
|
if (
|
||||||
|
!recApiVersion.api ||
|
||||||
|
typeof recApiVersion.api !== 'string' ||
|
||||||
|
!recApiVersion.url ||
|
||||||
|
typeof recApiVersion.url !== 'string'
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(recApiVersion)}`,
|
||||||
|
)
|
||||||
|
// in a forEach-loop use return instead of continue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO better to introduce the validation on entity-Level by https://github.com/typestack/class-validator
|
||||||
|
if (recApiVersion.api.length > 10 || recApiVersion.url.length > 255) {
|
||||||
|
logger.warn(
|
||||||
|
`received apiVersion with content longer than max length: ${JSON.stringify(
|
||||||
|
recApiVersion,
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
// in a forEach-loop use return instead of continue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
apiVersion: recApiVersion.api,
|
||||||
|
endPoint: recApiVersion.url,
|
||||||
|
publicKey: socket.remotePublicKey.toString('hex'),
|
||||||
|
lastAnnouncedAt: new Date(),
|
||||||
|
}
|
||||||
|
logger.debug(`upsert with variables=${JSON.stringify(variables)}`)
|
||||||
|
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
|
||||||
|
await DbCommunity.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(DbCommunity)
|
||||||
|
.values(variables)
|
||||||
|
.orUpdate({
|
||||||
|
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||||
|
overwrite: ['end_point', 'last_announced_at'],
|
||||||
|
})
|
||||||
|
.execute()
|
||||||
|
logger.info(`federation community upserted successfully...`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
|
||||||
|
recApiVersions,
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error on receiving data from socket:', e)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await server.listen()
|
await server.listen()
|
||||||
@ -93,7 +158,6 @@ export const startDHT = async (
|
|||||||
logger.info(`Found new peers: ${collectedPubKeys}`)
|
logger.info(`Found new peers: ${collectedPubKeys}`)
|
||||||
|
|
||||||
collectedPubKeys.forEach((remotePubKey) => {
|
collectedPubKeys.forEach((remotePubKey) => {
|
||||||
// publicKey here is keyPair.publicKey from above
|
|
||||||
const socket = node.connect(Buffer.from(remotePubKey, 'hex'))
|
const socket = node.connect(Buffer.from(remotePubKey, 'hex'))
|
||||||
|
|
||||||
// socket.once("connect", function () {
|
// socket.once("connect", function () {
|
||||||
@ -110,17 +174,12 @@ export const startDHT = async (
|
|||||||
})
|
})
|
||||||
|
|
||||||
socket.on('open', function () {
|
socket.on('open', function () {
|
||||||
// noiseSocket fully open with the other peer
|
socket.write(Buffer.from(JSON.stringify(ownApiVersions)))
|
||||||
// console.log("writing to socket");
|
|
||||||
socket.write(Buffer.from(`${nodeRand}`))
|
|
||||||
socket.write(Buffer.from(JSON.stringify(nodeAPI)))
|
|
||||||
successfulRequests.push(remotePubKey)
|
successfulRequests.push(remotePubKey)
|
||||||
})
|
})
|
||||||
// pipe it somewhere like any duplex stream
|
|
||||||
// process.stdin.pipe(noiseSocket).pipe(process.stdout)
|
|
||||||
})
|
})
|
||||||
}, POLLTIME)
|
}, POLLTIME)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err)
|
logger.error('DHT unexpected error:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1961,8 +1961,7 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// In the futrue this should not throw anymore
|
it('throws no error for the second confirmation', async () => {
|
||||||
it('throws an error for the second confirmation', async () => {
|
|
||||||
const r1 = mutate({
|
const r1 = mutate({
|
||||||
mutation: confirmContribution,
|
mutation: confirmContribution,
|
||||||
variables: {
|
variables: {
|
||||||
@ -1982,8 +1981,7 @@ describe('ContributionResolver', () => {
|
|||||||
)
|
)
|
||||||
await expect(r2).resolves.toEqual(
|
await expect(r2).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
// data: { confirmContribution: true },
|
data: { confirmContribution: true },
|
||||||
errors: [new GraphQLError('Creation was not successful.')],
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import {
|
|||||||
sendContributionConfirmedEmail,
|
sendContributionConfirmedEmail,
|
||||||
sendContributionRejectedEmail,
|
sendContributionRejectedEmail,
|
||||||
} from '@/emails/sendEmailVariants'
|
} from '@/emails/sendEmailVariants'
|
||||||
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ContributionResolver {
|
export class ContributionResolver {
|
||||||
@ -579,8 +580,10 @@ export class ContributionResolver {
|
|||||||
clientTimezoneOffset,
|
clientTimezoneOffset,
|
||||||
)
|
)
|
||||||
|
|
||||||
const receivedCallDate = new Date()
|
// acquire lock
|
||||||
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
|
|
||||||
|
const receivedCallDate = new Date()
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||||
@ -590,7 +593,7 @@ export class ContributionResolver {
|
|||||||
.select('transaction')
|
.select('transaction')
|
||||||
.from(DbTransaction, 'transaction')
|
.from(DbTransaction, 'transaction')
|
||||||
.where('transaction.userId = :id', { id: contribution.userId })
|
.where('transaction.userId = :id', { id: contribution.userId })
|
||||||
.orderBy('transaction.balanceDate', 'DESC')
|
.orderBy('transaction.id', 'DESC')
|
||||||
.getOne()
|
.getOne()
|
||||||
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||||
|
|
||||||
@ -639,10 +642,11 @@ export class ContributionResolver {
|
|||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`Creation was not successful: ${e}`)
|
logger.error('Creation was not successful', e)
|
||||||
throw new Error(`Creation was not successful.`)
|
throw new Error('Creation was not successful.')
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
|
releaseLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = new Event()
|
const event = new Event()
|
||||||
|
|||||||
@ -23,6 +23,11 @@ import { User } from '@entity/User'
|
|||||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
|
// mock semaphore to allow use fake timers
|
||||||
|
jest.mock('@/util/TRANSACTIONS_LOCK')
|
||||||
|
TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
|
||||||
|
|
||||||
let mutate: any, query: any, con: any
|
let mutate: any, query: any, con: any
|
||||||
let testEnv: any
|
let testEnv: any
|
||||||
@ -185,8 +190,7 @@ describe('TransactionLinkResolver', () => {
|
|||||||
describe('after one day', () => {
|
describe('after one day', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
|
||||||
setTimeout(() => {}, 1000 * 60 * 60 * 24)
|
|
||||||
jest.runAllTimers()
|
jest.runAllTimers()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { calculateDecay } from '@/util/decay'
|
|||||||
import { getUserCreation, validateContribution } from './util/creations'
|
import { getUserCreation, validateContribution } from './util/creations'
|
||||||
import { executeTransaction } from './TransactionResolver'
|
import { executeTransaction } from './TransactionResolver'
|
||||||
import QueryLinkResult from '@union/QueryLinkResult'
|
import QueryLinkResult from '@union/QueryLinkResult'
|
||||||
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
// TODO: do not export, test it inside the resolver
|
// TODO: do not export, test it inside the resolver
|
||||||
export const transactionLinkCode = (date: Date): string => {
|
export const transactionLinkCode = (date: Date): string => {
|
||||||
@ -165,10 +166,12 @@ export class TransactionLinkResolver {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
if (code.match(/^CL-/)) {
|
if (code.match(/^CL-/)) {
|
||||||
|
// acquire lock
|
||||||
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
logger.info('redeem contribution link...')
|
logger.info('redeem contribution link...')
|
||||||
|
const now = new Date()
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('REPEATABLE READ')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
@ -273,7 +276,7 @@ export class TransactionLinkResolver {
|
|||||||
.select('transaction')
|
.select('transaction')
|
||||||
.from(DbTransaction, 'transaction')
|
.from(DbTransaction, 'transaction')
|
||||||
.where('transaction.userId = :id', { id: user.id })
|
.where('transaction.userId = :id', { id: user.id })
|
||||||
.orderBy('transaction.balanceDate', 'DESC')
|
.orderBy('transaction.id', 'DESC')
|
||||||
.getOne()
|
.getOne()
|
||||||
let newBalance = new Decimal(0)
|
let newBalance = new Decimal(0)
|
||||||
|
|
||||||
@ -309,9 +312,11 @@ export class TransactionLinkResolver {
|
|||||||
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
|
releaseLock()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
const now = new Date()
|
||||||
const transactionLink = await DbTransactionLink.findOneOrFail({ code })
|
const transactionLink = await DbTransactionLink.findOneOrFail({ code })
|
||||||
const linkedUser = await DbUser.findOneOrFail(
|
const linkedUser = await DbUser.findOneOrFail(
|
||||||
{ id: transactionLink.userId },
|
{ id: transactionLink.userId },
|
||||||
@ -322,6 +327,9 @@ export class TransactionLinkResolver {
|
|||||||
throw new Error('Cannot redeem own transaction link.')
|
throw new Error('Cannot redeem own transaction link.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: The now check should be done within the semaphore lock,
|
||||||
|
// since the program might wait a while till it is ready to proceed
|
||||||
|
// writing the transaction.
|
||||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||||
throw new Error('Transaction Link is not valid anymore.')
|
throw new Error('Transaction Link is not valid anymore.')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -368,5 +368,74 @@ describe('send coins', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('more transactions to test semaphore', () => {
|
||||||
|
it('sends the coins four times in a row', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: sendCoins,
|
||||||
|
variables: {
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
amount: 10,
|
||||||
|
memo: 'first transaction',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
sendCoins: 'true',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: sendCoins,
|
||||||
|
variables: {
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
amount: 20,
|
||||||
|
memo: 'second transaction',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
sendCoins: 'true',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: sendCoins,
|
||||||
|
variables: {
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
amount: 30,
|
||||||
|
memo: 'third transaction',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
sendCoins: 'true',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: sendCoins,
|
||||||
|
variables: {
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
amount: 40,
|
||||||
|
memo: 'fourth transaction',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
sendCoins: 'true',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -36,6 +36,8 @@ import { BalanceResolver } from './BalanceResolver'
|
|||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
import { findUserByEmail } from './UserResolver'
|
import { findUserByEmail } from './UserResolver'
|
||||||
|
|
||||||
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
memo: string,
|
memo: string,
|
||||||
@ -62,6 +64,10 @@ export const executeTransaction = async (
|
|||||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// acquire lock
|
||||||
|
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||||
|
|
||||||
|
try {
|
||||||
// validate amount
|
// validate amount
|
||||||
const receivedCallDate = new Date()
|
const receivedCallDate = new Date()
|
||||||
const sendBalance = await calculateBalance(
|
const sendBalance = await calculateBalance(
|
||||||
@ -146,7 +152,9 @@ export const executeTransaction = async (
|
|||||||
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
||||||
eventTransactionReceive.transactionId = transactionReceive.id
|
eventTransactionReceive.transactionId = transactionReceive.id
|
||||||
eventTransactionReceive.amount = transactionReceive.amount
|
eventTransactionReceive.amount = transactionReceive.amount
|
||||||
await eventProtocol.writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive))
|
await eventProtocol.writeEvent(
|
||||||
|
new Event().setEventTransactionReceive(eventTransactionReceive),
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`Transaction was not successful: ${e}`)
|
logger.error(`Transaction was not successful: ${e}`)
|
||||||
@ -180,6 +188,9 @@ export const executeTransaction = async (
|
|||||||
}
|
}
|
||||||
logger.info(`finished executeTransaction successfully`)
|
logger.info(`finished executeTransaction successfully`)
|
||||||
return true
|
return true
|
||||||
|
} finally {
|
||||||
|
releaseLock()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
|
|||||||
190
backend/src/graphql/resolver/semaphore.test.ts
Normal file
190
backend/src/graphql/resolver/semaphore.test.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { logger } from '@test/testSetup'
|
||||||
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
|
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||||
|
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||||
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
|
import { creationFactory, nMonthsBefore } from '@/seeds/factory/creation'
|
||||||
|
import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers'
|
||||||
|
import {
|
||||||
|
confirmContribution,
|
||||||
|
createContribution,
|
||||||
|
createTransactionLink,
|
||||||
|
redeemTransactionLink,
|
||||||
|
login,
|
||||||
|
createContributionLink,
|
||||||
|
sendCoins,
|
||||||
|
} from '@/seeds/graphql/mutations'
|
||||||
|
|
||||||
|
let mutate: any, con: any
|
||||||
|
let testEnv: any
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testEnv = await testEnvironment()
|
||||||
|
mutate = testEnv.mutate
|
||||||
|
con = testEnv.con
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
await con.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('semaphore', () => {
|
||||||
|
let contributionLinkCode = ''
|
||||||
|
let bobsTransactionLinkCode = ''
|
||||||
|
let bibisTransactionLinkCode = ''
|
||||||
|
let bibisOpenContributionId = -1
|
||||||
|
let bobsOpenContributionId = -1
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const now = new Date()
|
||||||
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
await userFactory(testEnv, peterLustig)
|
||||||
|
await userFactory(testEnv, bobBaumeister)
|
||||||
|
await creationFactory(testEnv, {
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
amount: 1000,
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
creationDate: nMonthsBefore(new Date()),
|
||||||
|
confirmed: true,
|
||||||
|
})
|
||||||
|
await creationFactory(testEnv, {
|
||||||
|
email: 'bob@baumeister.de',
|
||||||
|
amount: 1000,
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
creationDate: nMonthsBefore(new Date()),
|
||||||
|
confirmed: true,
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
data: { createContributionLink: contributionLink },
|
||||||
|
} = await mutate({
|
||||||
|
mutation: createContributionLink,
|
||||||
|
variables: {
|
||||||
|
amount: new Decimal(200),
|
||||||
|
name: 'Test Contribution Link',
|
||||||
|
memo: 'Danke für deine Teilnahme an dem Test der Contribution Links',
|
||||||
|
cycle: 'ONCE',
|
||||||
|
validFrom: new Date(2022, 5, 18).toISOString(),
|
||||||
|
validTo: new Date(now.getFullYear() + 1, 7, 14).toISOString(),
|
||||||
|
maxAmountPerMonth: new Decimal(200),
|
||||||
|
maxPerCycle: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
contributionLinkCode = 'CL-' + contributionLink.code
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
data: { createTransactionLink: bobsLink },
|
||||||
|
} = await mutate({
|
||||||
|
mutation: createTransactionLink,
|
||||||
|
variables: {
|
||||||
|
email: 'bob@baumeister.de',
|
||||||
|
amount: 20,
|
||||||
|
memo: 'Bobs Link',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
data: { createContribution: bobsContribution },
|
||||||
|
} = await mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
creationDate: contributionDateFormatter(new Date()),
|
||||||
|
amount: 200,
|
||||||
|
memo: 'Bobs Contribution',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
data: { createTransactionLink: bibisLink },
|
||||||
|
} = await mutate({
|
||||||
|
mutation: createTransactionLink,
|
||||||
|
variables: {
|
||||||
|
amount: 20,
|
||||||
|
memo: 'Bibis Link',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
data: { createContribution: bibisContribution },
|
||||||
|
} = await mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
creationDate: contributionDateFormatter(new Date()),
|
||||||
|
amount: 200,
|
||||||
|
memo: 'Bibis Contribution',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
bobsTransactionLinkCode = bobsLink.code
|
||||||
|
bibisTransactionLinkCode = bibisLink.code
|
||||||
|
bibisOpenContributionId = bibisContribution.id
|
||||||
|
bobsOpenContributionId = bobsContribution.id
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates a lot of transactions without errors', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
const bibiRedeemContributionLink = mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: { code: contributionLinkCode },
|
||||||
|
})
|
||||||
|
const redeemBobsLink = mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: { code: bobsTransactionLinkCode },
|
||||||
|
})
|
||||||
|
const bibisTransaction = mutate({
|
||||||
|
mutation: sendCoins,
|
||||||
|
variables: { email: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
const bobRedeemContributionLink = mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: { code: contributionLinkCode },
|
||||||
|
})
|
||||||
|
const redeemBibisLink = mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: { code: bibisTransactionLinkCode },
|
||||||
|
})
|
||||||
|
const bobsTransaction = mutate({
|
||||||
|
mutation: sendCoins,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
const confirmBibisContribution = mutate({
|
||||||
|
mutation: confirmContribution,
|
||||||
|
variables: { id: bibisOpenContributionId },
|
||||||
|
})
|
||||||
|
const confirmBobsContribution = mutate({
|
||||||
|
mutation: confirmContribution,
|
||||||
|
variables: { id: bobsOpenContributionId },
|
||||||
|
})
|
||||||
|
await expect(bibiRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
|
||||||
|
await expect(redeemBobsLink).resolves.toMatchObject({ errors: undefined })
|
||||||
|
await expect(bibisTransaction).resolves.toMatchObject({ errors: undefined })
|
||||||
|
await expect(bobRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
|
||||||
|
await expect(redeemBibisLink).resolves.toMatchObject({ errors: undefined })
|
||||||
|
await expect(bobsTransaction).resolves.toMatchObject({ errors: undefined })
|
||||||
|
await expect(confirmBibisContribution).resolves.toMatchObject({ errors: undefined })
|
||||||
|
await expect(confirmBobsContribution).resolves.toMatchObject({ errors: undefined })
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -20,6 +20,9 @@ async function main() {
|
|||||||
|
|
||||||
// start DHT hyperswarm when DHT_TOPIC is set in .env
|
// start DHT hyperswarm when DHT_TOPIC is set in .env
|
||||||
if (CONFIG.FEDERATION_DHT_TOPIC) {
|
if (CONFIG.FEDERATION_DHT_TOPIC) {
|
||||||
|
if (CONFIG.FEDERATION_COMMUNITY_URL === null) {
|
||||||
|
throw Error(`Config-Error: missing configuration of property FEDERATION_COMMUNITY_URL`)
|
||||||
|
}
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(
|
console.log(
|
||||||
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
||||||
|
|||||||
@ -75,10 +75,7 @@ const run = async () => {
|
|||||||
|
|
||||||
// create GDD
|
// create GDD
|
||||||
for (let i = 0; i < creations.length; i++) {
|
for (let i = 0; i < creations.length; i++) {
|
||||||
const now = new Date().getTime() // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
|
|
||||||
await creationFactory(seedClient, creations[i])
|
await creationFactory(seedClient, creations[i])
|
||||||
// eslint-disable-next-line no-empty
|
|
||||||
while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
|
|
||||||
}
|
}
|
||||||
logger.info('##seed## seeding all creations successful...')
|
logger.info('##seed## seeding all creations successful...')
|
||||||
|
|
||||||
|
|||||||
4
backend/src/util/TRANSACTIONS_LOCK.ts
Normal file
4
backend/src/util/TRANSACTIONS_LOCK.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Semaphore } from 'await-semaphore'
|
||||||
|
|
||||||
|
const CONCURRENT_TRANSACTIONS = 1
|
||||||
|
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)
|
||||||
@ -20,7 +20,7 @@ async function calculateBalance(
|
|||||||
time: Date,
|
time: Date,
|
||||||
transactionLink?: dbTransactionLink | null,
|
transactionLink?: dbTransactionLink | null,
|
||||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } })
|
||||||
if (!lastTransaction) return null
|
if (!lastTransaction) return null
|
||||||
|
|
||||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||||
|
|||||||
@ -1643,6 +1643,11 @@ asynckit@^0.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
|
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
|
||||||
|
|
||||||
|
await-semaphore@^0.1.3:
|
||||||
|
version "0.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3"
|
||||||
|
integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==
|
||||||
|
|
||||||
axios@^0.21.1:
|
axios@^0.21.1:
|
||||||
version "0.21.4"
|
version "0.21.4"
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
||||||
|
|||||||
42
database/entity/0058-add_communities_table/Community.ts
Normal file
42
database/entity/0058-add_communities_table/Community.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm'
|
||||||
|
|
||||||
|
@Entity('communities')
|
||||||
|
export class Community extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 64, default: null, nullable: true })
|
||||||
|
publicKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'api_version', length: 10, nullable: false })
|
||||||
|
apiVersion: string
|
||||||
|
|
||||||
|
@Column({ name: 'end_point', length: 255, nullable: false })
|
||||||
|
endPoint: string
|
||||||
|
|
||||||
|
@Column({ name: 'last_announced_at', type: 'datetime', nullable: false })
|
||||||
|
lastAnnouncedAt: Date
|
||||||
|
|
||||||
|
@CreateDateColumn({
|
||||||
|
name: 'created_at',
|
||||||
|
type: 'datetime',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn({
|
||||||
|
name: 'updated_at',
|
||||||
|
type: 'datetime',
|
||||||
|
onUpdate: 'CURRENT_TIMESTAMP(3)',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
updatedAt: Date | null
|
||||||
|
}
|
||||||
1
database/entity/Community.ts
Normal file
1
database/entity/Community.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Community } from './0058-add_communities_table/Community'
|
||||||
@ -9,6 +9,7 @@ import { UserContact } from './UserContact'
|
|||||||
import { Contribution } from './Contribution'
|
import { Contribution } from './Contribution'
|
||||||
import { EventProtocol } from './EventProtocol'
|
import { EventProtocol } from './EventProtocol'
|
||||||
import { ContributionMessage } from './ContributionMessage'
|
import { ContributionMessage } from './ContributionMessage'
|
||||||
|
import { Community } from './Community'
|
||||||
|
|
||||||
export const entities = [
|
export const entities = [
|
||||||
Contribution,
|
Contribution,
|
||||||
@ -22,4 +23,5 @@ export const entities = [
|
|||||||
EventProtocol,
|
EventProtocol,
|
||||||
ContributionMessage,
|
ContributionMessage,
|
||||||
UserContact,
|
UserContact,
|
||||||
|
Community,
|
||||||
]
|
]
|
||||||
|
|||||||
28
database/migrations/0058-add_communities_table.ts
Normal file
28
database/migrations/0058-add_communities_table.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/* MIGRATION TO CREATE THE FEDERATION COMMUNITY TABLES
|
||||||
|
*
|
||||||
|
* This migration creates the `community` and 'communityfederation' tables in the `apollo` database (`gradido_community`).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn(`
|
||||||
|
CREATE TABLE communities (
|
||||||
|
id int unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
public_key binary(64),
|
||||||
|
api_version varchar(10) NOT NULL,
|
||||||
|
end_point varchar(255) NOT NULL,
|
||||||
|
last_announced_at datetime(3) NOT NULL,
|
||||||
|
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at datetime(3),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY public_api_key (public_key, api_version)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
// write downgrade logic as parameter of queryFn
|
||||||
|
await queryFn(`DROP TABLE communities;`)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user