Merge remote-tracking branch 'origin/2946-feature-x-com-3-introduce-business-communities' into 2956-feature-x-com-4-introduce-public-community-info-handshake

This commit is contained in:
Claus-Peter Huebner 2023-05-25 00:04:18 +02:00
commit 3809faaaac
7 changed files with 285 additions and 150 deletions

View File

@ -27,7 +27,8 @@ module.exports = {
},
},
rules: {
'no-console': ['error'],
'no-console': 'error',
camelcase: ['error', { allow: ['FederationClient_*'] }],
'no-debugger': 'error',
'prettier/prettier': [
'error',
@ -184,6 +185,7 @@ module.exports = {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json', '**/tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it
// eslint-disable-next-line camelcase
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
},

View File

@ -0,0 +1,55 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ApiVersionType } from '@/federation/enum/apiVersionType'
import { FederationClient_1_0 } from './FederationClient_1_0'
import { FederationClient_1_1 } from './FederationClient_1_1'
type FederationClientType = FederationClient_1_0 | FederationClient_1_1
interface ClientInstance {
id: number
// eslint-disable-next-line no-use-before-define
client: FederationClientType
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class FederationClient {
private static instanceArray: ClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
private static createFederationClient = (dbCom: DbFederatedCommunity) => {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return new FederationClient_1_0(dbCom)
case ApiVersionType.V1_1:
return new FederationClient_1_1(dbCom)
default:
return null
}
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(dbCom: DbFederatedCommunity): FederationClientType | null {
const instance = FederationClient.instanceArray.find((instance) => instance.id === dbCom.id)
if (instance) {
return instance.client
}
const client = FederationClient.createFederationClient(dbCom)
if (client) {
FederationClient.instanceArray.push({ id: dbCom.id, client } as ClientInstance)
}
return client
}
}

View File

@ -0,0 +1,48 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { GraphQLClient } from 'graphql-request'
import { getPublicKey } from '@/federation/query/getPublicKey'
import { backendLogger as logger } from '@/server/logger'
export class FederationClient_1_0 {
dbCom: DbFederatedCommunity
endpoint: string
client: GraphQLClient
constructor(dbCom: DbFederatedCommunity) {
this.dbCom = dbCom
this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
dbCom.apiVersion
}/`
this.client = new GraphQLClient(this.endpoint, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
}
getPublicKey = async (): Promise<string | undefined> => {
logger.info('Federation: getPublicKey from endpoint', this.endpoint)
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(getPublicKey, {})
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!data?.getPublicKey?.publicKey) {
logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint)
return
}
logger.info(
'Federation: getPublicKey successful from endpoint',
this.endpoint,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
data.getPublicKey.publicKey,
)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return data.getPublicKey.publicKey
} catch (err) {
logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint)
}
}
}

View File

@ -0,0 +1,3 @@
import { FederationClient_1_0 } from './FederationClient_1_0'
export class FederationClient_1_1 extends FederationClient_1_0 {}

View File

@ -83,6 +83,7 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
@ -160,6 +161,7 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity)
.values(variables2)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
@ -198,6 +200,7 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity)
.values(variables3)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})

View File

@ -1,12 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import {
CommunityApi,
startDHT,
writeFederatedHomeCommunityEntries,
writeHomeCommunityEntry,
} from './index'
import { startDHT } from './index'
import DHT from '@hyperswarm/dht'
import CONFIG from '@/config'
import { logger } from '@test/testSetup'
@ -121,6 +116,9 @@ describe('federation', () => {
const hashSpy = jest.spyOn(DHT, 'hash')
const keyPairSpy = jest.spyOn(DHT, 'keyPair')
beforeEach(async () => {
CONFIG.FEDERATION_COMMUNITY_URL = 'https://test.gradido.net'
CONFIG.COMMUNITY_NAME = 'Gradido Test Community'
CONFIG.COMMUNITY_DESCRIPTION = 'Community to test the federation'
DHT.mockClear()
jest.clearAllMocks()
await cleanDB()
@ -139,6 +137,64 @@ describe('federation', () => {
expect(DHT).toBeCalledWith({ keyPair: keyPairMock })
})
it('stores the home community in community table ', async () => {
const result = await DbCommunity.find()
expect(result).toEqual([
expect.objectContaining({
id: expect.any(Number),
foreign: false,
url: 'https://test.gradido.net/api/',
publicKey: expect.any(Buffer),
communityUuid: expect.any(String),
authenticatedAt: null,
name: 'Gradido Test Community',
description: 'Community to test the federation',
creationDate: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
])
expect(validateUUID(result[0].communityUuid ? result[0].communityUuid : '')).toEqual(true)
expect(versionUUID(result[0].communityUuid ? result[0].communityUuid : '')).toEqual(4)
})
it('creates 3 entries in table federated_communities', async () => {
const result = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await expect(result).toHaveLength(3)
await expect(result).toEqual([
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '1_0',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '1_1',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '2_0',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
])
})
describe('DHT node', () => {
it('creates a server', () => {
expect(nodeCreateServerMock).toBeCalled()
@ -162,131 +218,6 @@ describe('federation', () => {
})
})
describe('home community', () => {
it('one in table communities', async () => {
const result = await DbCommunity.find({ foreign: false })
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
foreign: false,
url: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
publicKey: expect.any(Buffer),
communityUuid: expect.any(String),
authenticatedAt: null,
name: CONFIG.COMMUNITY_NAME,
description: CONFIG.COMMUNITY_DESCRIPTION,
creationDate: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
const valUUID = validateUUID(
result[0].communityUuid != null ? result[0].communityUuid : '',
)
const verUUID = versionUUID(
result[0].communityUuid != null ? result[0].communityUuid : '',
)
expect(valUUID).toEqual(true)
expect(verUUID).toEqual(4)
})
it('update the one in table communities', async () => {
const resultBefore = await DbCommunity.find({ foreign: false })
expect(resultBefore).toHaveLength(1)
const modifiedCom = DbCommunity.create()
modifiedCom.description = 'updated description'
modifiedCom.name = 'update name'
modifiedCom.publicKey = Buffer.from(
'1234567891abcdef7892abcdef7893abcdef7894abcdef7895abcdef7896abcd',
)
modifiedCom.url = 'updated url'
await DbCommunity.update(modifiedCom, { id: resultBefore[0].id })
await writeHomeCommunityEntry(modifiedCom.publicKey.toString())
const resultAfter = await DbCommunity.find({ foreign: false })
expect(resultAfter).toHaveLength(1)
expect(resultAfter).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: resultBefore[0].id,
foreign: false,
url: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
publicKey: modifiedCom.publicKey,
communityUuid: resultBefore[0].communityUuid,
authenticatedAt: null,
name: CONFIG.COMMUNITY_NAME,
description: CONFIG.COMMUNITY_DESCRIPTION,
creationDate: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
}),
]),
)
})
})
// skipped because ot timing problems in testframework
describe.skip('federated home community', () => {
it('three in table federated_communities', async () => {
const homeApiVersions: CommunityApi[] = await writeFederatedHomeCommunityEntries(
keyPairMock.publicKey.toString('hex'),
)
expect(homeApiVersions).toEqual(
expect.arrayContaining([
expect.objectContaining({
api: '1_0',
url: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
}),
expect.objectContaining({
api: '1_1',
url: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
}),
expect.objectContaining({
api: '2_0',
url: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
}),
]),
)
const result = await DbFederatedCommunity.find({ foreign: false })
expect(result).toHaveLength(3)
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '1_0',
endPoint: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '1_1',
endPoint: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '2_0',
endPoint: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
})
describe('server connection event', () => {
beforeEach(() => {
serverEventMocks.connection({
@ -918,15 +849,15 @@ describe('federation', () => {
JSON.stringify([
{
api: '1_0',
url: 'http://localhost/api/',
url: 'https://test.gradido.net/api/',
},
{
api: '1_1',
url: 'http://localhost/api/',
url: 'https://test.gradido.net/api/',
},
{
api: '2_0',
url: 'http://localhost/api/',
url: 'https://test.gradido.net/api/',
},
]),
),
@ -936,5 +867,101 @@ describe('federation', () => {
})
})
})
describe('restart DHT', () => {
let homeCommunity: DbCommunity
let federatedCommunities: DbFederatedCommunity[]
describe('without changes', () => {
beforeEach(async () => {
DHT.mockClear()
jest.clearAllMocks()
homeCommunity = (await DbCommunity.find())[0]
federatedCommunities = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await startDHT(TEST_TOPIC)
})
it('does not change home community in community table except updated at column ', async () => {
await expect(DbCommunity.find()).resolves.toEqual([
{
...homeCommunity,
updatedAt: expect.any(Date),
},
])
})
it('rewrites the 3 entries in table federated_communities', async () => {
const result = await DbFederatedCommunity.find()
await expect(result).toHaveLength(3)
await expect(result).toEqual([
{
...federatedCommunities[0],
id: expect.any(Number),
createdAt: expect.any(Date),
},
{
...federatedCommunities[1],
id: expect.any(Number),
createdAt: expect.any(Date),
},
{
...federatedCommunities[2],
id: expect.any(Number),
createdAt: expect.any(Date),
},
])
})
})
describe('changeing URL, name and description', () => {
beforeEach(async () => {
CONFIG.FEDERATION_COMMUNITY_URL = 'https://test2.gradido.net'
CONFIG.COMMUNITY_NAME = 'Second Gradido Test Community'
CONFIG.COMMUNITY_DESCRIPTION = 'Another Community to test the federation'
DHT.mockClear()
jest.clearAllMocks()
homeCommunity = (await DbCommunity.find())[0]
federatedCommunities = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await startDHT(TEST_TOPIC)
})
it('updates URL, name, description and updated at columns ', async () => {
await expect(DbCommunity.find()).resolves.toEqual([
{
...homeCommunity,
url: 'https://test2.gradido.net/api/',
name: 'Second Gradido Test Community',
description: 'Another Community to test the federation',
updatedAt: expect.any(Date),
},
])
})
it('rewrites the 3 entries in table federated_communities with new endpoint', async () => {
const result = await DbFederatedCommunity.find()
await expect(result).toHaveLength(3)
await expect(result).toEqual([
{
...federatedCommunities[0],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
{
...federatedCommunities[1],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
{
...federatedCommunities[2],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
])
})
})
})
})
})

View File

@ -33,13 +33,12 @@ export const startDHT = async (topic: string): Promise<void> => {
try {
const TOPIC = DHT.hash(Buffer.from(topic))
const keyPair = DHT.keyPair(getSeed())
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
const pubKeyString = keyPair.publicKey.toString('hex')
logger.info(`keyPairDHT: publicKey=${pubKeyString}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
await writeHomeCommunityEntry(keyPair.publicKey.toString('hex'))
await writeHomeCommunityEntry(pubKeyString)
const ownApiVersions = await writeFederatedHomeCommunityEntries(
keyPair.publicKey.toString('hex'),
)
const ownApiVersions = await writeFederatedHomeCommunityEntries(pubKeyString)
logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair })
@ -146,7 +145,7 @@ export const startDHT = async (topic: string): Promise<void> => {
data.peers.forEach((peer: any) => {
const pubKey = peer.publicKey.toString('hex')
if (
pubKey !== keyPair.publicKey.toString('hex') &&
pubKey !== pubKeyString &&
!successfulRequests.includes(pubKey) &&
!errorfulRequests.includes(pubKey) &&
!collectedPubKeys.includes(pubKey)
@ -197,17 +196,15 @@ export async function writeFederatedHomeCommunityEntries(pubKey: string): Promis
})
try {
// first remove privious existing homeCommunity entries
DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
for (let i = 0; i < homeApiVersions.length; i++) {
await DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
for (const homeApiVersion of homeApiVersions) {
const homeCom = DbFederatedCommunity.create()
homeCom.foreign = false
homeCom.apiVersion = homeApiVersions[i].api
homeCom.endPoint = homeApiVersions[i].url
homeCom.apiVersion = homeApiVersion.api
homeCom.endPoint = homeApiVersion.url
homeCom.publicKey = Buffer.from(pubKey)
await DbFederatedCommunity.insert(homeCom)
logger.info(
`federation home-community inserted successfully: ${JSON.stringify(homeApiVersions[i])}`,
)
logger.info(`federation home-community inserted successfully:`, homeApiVersion)
}
} catch (err) {
throw new Error(`Federation: Error writing federated HomeCommunity-Entries: ${err}`)
@ -233,7 +230,7 @@ export async function writeHomeCommunityEntry(pubKey: string): Promise<void> {
homeCom.name = CONFIG.COMMUNITY_NAME
homeCom.description = CONFIG.COMMUNITY_DESCRIPTION
await DbCommunity.save(homeCom)
logger.info(`home-community updated successfully: ${JSON.stringify(homeCom)}`)
logger.info(`home-community updated successfully:`, homeCom)
} else {
// insert a new homecommunity entry including a new ID and a new but ensured unique UUID
homeCom = new DbCommunity()
@ -245,7 +242,7 @@ export async function writeHomeCommunityEntry(pubKey: string): Promise<void> {
homeCom.description = CONFIG.COMMUNITY_DESCRIPTION
homeCom.creationDate = new Date()
await DbCommunity.insert(homeCom)
logger.info(`home-community inserted successfully: ${JSON.stringify(homeCom)}`)
logger.info(`home-community inserted successfully:`, homeCom)
}
} catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entry: ${err}`)