fix community resolver test and riddle why db had stopped working in test

This commit is contained in:
einhorn_b 2023-10-31 16:12:19 +01:00
parent 0484b15464
commit f6264879ba
20 changed files with 197 additions and 129 deletions

View File

@ -17,6 +17,7 @@ module.exports = {
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
'@controller/(.*)': '<rootDir>/src/controller/$1',
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
'@resolver/(.*)': '<rootDir>/src/graphql/resolver/$1',
'@input/(.*)': '<rootDir>/src/graphql/input/$1',
'@proto/(.*)': '<rootDir>/src/proto/$1',

View File

@ -21,6 +21,7 @@ export class AccountFactory {
account.type = type.valueOf()
account.createdAt = createdAt
account.balance = new Decimal(0)
account.balanceCreatedAt = new Decimal(0)
return account
}

View File

@ -8,12 +8,12 @@ export const AccountRepository = getDataSource()
.getRepository(Account)
.extend({
findAccountsByPublicKeys(publicKeys: Buffer[]): Promise<Account[]> {
return Account.findBy({ derive2Pubkey: In(publicKeys) })
return this.findBy({ derive2Pubkey: In(publicKeys) })
},
async findAccountByPublicKey(publicKey: Buffer | undefined): Promise<Account | undefined> {
if (!publicKey) return undefined
return (await Account.findOneBy({ derive2Pubkey: Buffer.from(publicKey) })) ?? undefined
return (await this.findOneBy({ derive2Pubkey: Buffer.from(publicKey) })) ?? undefined
},
async findAccountByUserIdentifier({

View File

@ -16,14 +16,14 @@ export const CommunityRepository = getDataSource()
async isExist(community: CommunityDraft | string): Promise<boolean> {
const iotaTopic =
community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community
const result = await Community.find({
const result = await this.find({
where: { iotaTopic },
})
return result.length > 0
},
async findByCommunityArg({ uuid, foreign, confirmed }: CommunityArg): Promise<Community[]> {
return await Community.find({
return await this.find({
where: {
...(uuid && { iotaTopic: iotaTopicFromCommunityUUID(uuid) }),
...(foreign && { foreign }),
@ -33,15 +33,15 @@ export const CommunityRepository = getDataSource()
},
async findByCommunityUuid(communityUuid: string): Promise<Community | null> {
return await Community.findOneBy({ iotaTopic: iotaTopicFromCommunityUUID(communityUuid) })
return await this.findOneBy({ iotaTopic: iotaTopicFromCommunityUUID(communityUuid) })
},
async findByIotaTopic(iotaTopic: string): Promise<Community | null> {
return await Community.findOneBy({ iotaTopic })
return await this.findOneBy({ iotaTopic })
},
findCommunitiesByTopics(topics: string[]): Promise<Community[]> {
return Community.findBy({ iotaTopic: In(topics) })
return this.findBy({ iotaTopic: In(topics) })
},
async getCommunityForUserIdentifier(
@ -51,18 +51,18 @@ export const CommunityRepository = getDataSource()
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community uuid not set')
}
return (
(await Community.findOneBy({
(await this.findOneBy({
iotaTopic: iotaTopicFromCommunityUUID(identifier.communityUuid),
})) ?? undefined
)
},
findAll(select: FindOptionsSelect<Community>): Promise<Community[]> {
return Community.find({ select })
return this.find({ select })
},
async loadHomeCommunityKeyPair(): Promise<KeyPair> {
const community = await Community.findOneOrFail({
const community = await this.findOneOrFail({
where: { foreign: false },
select: { rootChaincode: true, rootPubkey: true, rootPrivkey: true },
})

View File

@ -43,6 +43,9 @@ export const TransactionRepository = getDataSource()
return { existingTransactions, missingMessageIdsHex }
},
async removeConfirmedTransaction(transactions: Transaction[]): Promise<Transaction[]> {
return transactions.filter((transaction: Transaction) => transaction.runningHash.length === 0)
return transactions.filter(
(transaction: Transaction) =>
transaction.runningHash === undefined || transaction.runningHash.length === 0,
)
},
})

View File

@ -0,0 +1,23 @@
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { getDataSource } from '@/typeorm/DataSource'
import { Account } from '@entity/Account'
import { User } from '@entity/User'
export const UserRepository = getDataSource()
.getRepository(User)
.extend({
async findAccountByUserIdentifier({
uuid,
accountNr,
}: UserIdentifier): Promise<Account | undefined> {
const user = await this.findOne({
where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } },
relations: { accounts: true },
})
if (user && user.accounts?.length === 1) {
const account = user.accounts[0]
account.user = user
return account
}
},
})

View File

@ -2,7 +2,6 @@ import { Field, Message } from 'protobufjs'
import { GradidoTransaction } from './GradidoTransaction'
import { TimestampSeconds } from './TimestampSeconds'
import { base64ToBuffer } from '@/utils/typeConverter'
import Long from 'long'
/*
id will be set by Node server

View File

@ -1,34 +1,35 @@
import 'reflect-metadata'
import { ApolloServer } from '@apollo/server'
import { TestDB } from '@test/TestDB'
import { createApolloTestServer } from '@test/ApolloServerMock'
import assert from 'assert'
import { TestDB } from '@test/TestDB'
import { TransactionResult } from '../model/TransactionResult'
import { TransactionResult } from '@model/TransactionResult'
import { CONFIG } from '@/config'
CONFIG.IOTA_HOME_COMMUNITY_SEED = 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899'
const con = TestDB.instance
jest.mock('@typeorm/DataSource', () => ({
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
}))
let apolloTestServer: ApolloServer
jest.mock('@typeorm/DataSource', () => ({
getDataSource: () => TestDB.instance.dbConnect,
}))
describe('graphql/resolver/CommunityResolver', () => {
beforeAll(async () => {
await con.setupTestDB()
apolloTestServer = await createApolloTestServer()
})
afterAll(async () => {
await con.teardownTestDB()
})
describe('tests with db', () => {
beforeAll(async () => {
await TestDB.instance.setupTestDB()
// apolloTestServer = await createApolloTestServer()
})
afterAll(async () => {
await TestDB.instance.teardownTestDB()
})
it('test add foreign community', async () => {
const response = await apolloTestServer.executeOperation({
query: 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed} }',
query:
'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed, error {message}} }',
variables: {
input: {
uuid: '3d813cbb-37fb-42ba-91df-831e1593ac29',
@ -45,7 +46,8 @@ describe('graphql/resolver/CommunityResolver', () => {
it('test add home community', async () => {
const response = await apolloTestServer.executeOperation({
query: 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed} }',
query:
'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed, error {message}} }',
variables: {
input: {
uuid: '3d823cad-37fb-41cd-91df-152e1593ac29',

View File

@ -1,8 +1,9 @@
import 'reflect-metadata'
import { ApolloServer } from '@apollo/server'
import { TestDB } from '@test/TestDB'
import { createApolloTestServer } from '@test/ApolloServerMock'
import assert from 'assert'
import { TransactionResult } from '../model/TransactionResult'
import { TransactionResult } from '@model/TransactionResult'
let apolloTestServer: ApolloServer
@ -14,26 +15,24 @@ jest.mock('@/client/IotaClient', () => {
}
})
jest.mock('@typeorm/DataSource', () => ({
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
}))
describe('Transaction Resolver Test', () => {
beforeAll(async () => {
apolloTestServer = await createApolloTestServer()
await TestDB.instance.setupTestDB()
})
it('test version query', async () => {
const response = await apolloTestServer.executeOperation({
query: '{ version }',
})
// Note the use of Node's assert rather than Jest's expect; if using
// TypeScript, `assert`` will appropriately narrow the type of `body`
// and `expect` will not.
// Source: https://www.apollographql.com/docs/apollo-server/testing/testing
assert(response.body.kind === 'single')
expect(response.body.singleResult.errors).toBeUndefined()
expect(response.body.singleResult.data?.version).toBe('0.1')
afterAll(async () => {
await TestDB.instance.teardownTestDB()
})
it('test mocked sendTransaction', async () => {
const response = await apolloTestServer.executeOperation({
query:
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
variables: {
input: {
senderUser: {
@ -45,21 +44,20 @@ describe('Transaction Resolver Test', () => {
type: 'SEND',
amount: '10',
createdAt: '2012-04-17T17:12:00Z',
backendTransactionId: 1,
},
},
})
assert(response.body.kind === 'single')
expect(response.body.singleResult.errors).toBeUndefined()
const transactionResult = response.body.singleResult.data?.sendTransaction as TransactionResult
expect(transactionResult.messageId).toBe(
'5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710',
)
expect(transactionResult.succeed).toBe(true)
})
it('test mocked sendTransaction invalid transactionType ', async () => {
const response = await apolloTestServer.executeOperation({
query:
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
variables: {
input: {
senderUser: {
@ -71,6 +69,7 @@ describe('Transaction Resolver Test', () => {
type: 'INVALID',
amount: '10',
createdAt: '2012-04-17T17:12:00Z',
backendTransactionId: 1,
},
},
})
@ -88,7 +87,7 @@ describe('Transaction Resolver Test', () => {
it('test mocked sendTransaction invalid amount ', async () => {
const response = await apolloTestServer.executeOperation({
query:
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
variables: {
input: {
senderUser: {
@ -100,6 +99,7 @@ describe('Transaction Resolver Test', () => {
type: 'SEND',
amount: 'no number',
createdAt: '2012-04-17T17:12:00Z',
backendTransactionId: 1,
},
},
})
@ -117,7 +117,7 @@ describe('Transaction Resolver Test', () => {
it('test mocked sendTransaction invalid created date ', async () => {
const response = await apolloTestServer.executeOperation({
query:
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
variables: {
input: {
senderUser: {
@ -129,6 +129,7 @@ describe('Transaction Resolver Test', () => {
type: 'SEND',
amount: '10',
createdAt: 'not valid',
backendTransactionId: 1,
},
},
})
@ -156,7 +157,7 @@ describe('Transaction Resolver Test', () => {
it('test mocked sendTransaction missing creationDate for contribution', async () => {
const response = await apolloTestServer.executeOperation({
query:
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
variables: {
input: {
senderUser: {
@ -168,6 +169,7 @@ describe('Transaction Resolver Test', () => {
type: 'CREATION',
amount: '10',
createdAt: '2012-04-17T17:12:00Z',
backendTransactionId: 1,
},
},
})

View File

@ -10,6 +10,7 @@ export const schema = async (): Promise<GraphQLSchema> => {
return buildSchema({
resolvers: [TransactionResolver, CommunityResolver],
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
emitSchemaFile: true,
validate: {
validationError: { target: false },
skipMissingProperties: true,

View File

@ -24,7 +24,7 @@ export class AddCommunityContext {
}
public async run(): Promise<void> {
this.communityRole.create(this.communityDraft, this.iotaTopic)
await this.communityRole.create(this.communityDraft, this.iotaTopic)
await this.communityRole.store()
}
}

View File

@ -10,7 +10,7 @@ export abstract class CommunityRole {
this.self = Community.create()
}
public create(communityDraft: CommunityDraft, topic: string): void {
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
this.self.iotaTopic = topic
this.self.createdAt = new Date(communityDraft.createdAt)
this.self.foreign = communityDraft.foreign

View File

@ -35,12 +35,15 @@ export class HomeCommunityRole extends CommunityRole {
public async store(): Promise<Community> {
try {
console.log('store transaction: %s', JSON.stringify(this.transactionRecipe, null, 2))
return await getDataSource().transaction(async (transactionalEntityManager) => {
const community = await transactionalEntityManager.save(this.self)
await transactionalEntityManager.save(this.transactionRecipe)
return await transactionalEntityManager.save(this.self)
return community
})
} catch (error) {
logger.error('error saving home community into db: %s', error)
console.log(error)
throw new TransactionError(
TransactionErrorType.DB_ERROR,
'error saving home community into db',

View File

@ -1,6 +1,7 @@
import { AccountRepository } from '@/data/Account.repository'
import { KeyPair } from '@/data/KeyPair'
import { TransactionBuilder } from '@/data/Transaction.builder'
import { UserRepository } from '@/data/User.repository'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
@ -10,7 +11,8 @@ import { Transaction } from '@entity/Transaction'
export class TransactionRecipeRole {
protected transactionBuilder: TransactionBuilder
construct() {
public constructor() {
this.transactionBuilder = new TransactionBuilder()
}
@ -20,14 +22,14 @@ export class TransactionRecipeRole {
// loading signing and recipient account
// TODO: look for ways to use only one db call for both
const signingAccount = await AccountRepository.findAccountByUserIdentifier(senderUser)
const signingAccount = await UserRepository.findAccountByUserIdentifier(senderUser)
if (!signingAccount) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"couldn't found sender user account in db",
)
}
const recipientAccount = await AccountRepository.findAccountByUserIdentifier(recipientUser)
const recipientAccount = await UserRepository.findAccountByUserIdentifier(recipientUser)
if (!recipientAccount) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,

View File

@ -3,12 +3,7 @@ import { randombytes_buf } from 'sodium-native'
import { CONFIG } from '../config'
import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
// https://www.npmjs.com/package/bip32-ed25519?activeTab=code
import {
generateFromSeed,
derivePrivate,
sign as ed25519Sign,
verify as ed25519Verify,
} from 'bip32-ed25519'
import { generateFromSeed, derivePrivate, sign as ed25519Sign } from 'bip32-ed25519'
import { logger } from '@/server/logger'
import { LogError } from '@/server/LogError'
import { KeyPair } from '@/data/KeyPair'

View File

@ -11,6 +11,7 @@ import { logger as dltLogger } from './logger'
import { Logger } from 'log4js'
import cors from 'cors'
import bodyParser from 'body-parser'
import { Connection } from '@/typeorm/DataSource'
type ServerDef = { apollo: ApolloServer; app: Express }
@ -27,6 +28,8 @@ const createServer = async (
logger.addContext('user', 'unknown')
logger.debug('createServer...')
// connect to db and test db version
await Connection.getInstance().init()
// Express Server
const app = express()

View File

@ -1,28 +0,0 @@
import { Migration } from '@entity/Migration'
import { logger } from '@/server/logger'
const getDBVersion = async (): Promise<string | null> => {
try {
const [dbVersion] = await Migration.find({ order: { version: 'DESC' }, take: 1 })
return dbVersion ? dbVersion.fileName : null
} catch (error) {
logger.error(error)
return null
}
}
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
const dbVersion = await getDBVersion()
if (!dbVersion?.includes(DB_VERSION)) {
logger.error(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion ?? 'None'
}`,
)
return false
}
return true
}
export { checkDBVersion, getDBVersion }

View File

@ -4,23 +4,84 @@ import { DataSource as DBDataSource, FileLogger } from '@dbTools/typeorm'
import { entities } from '@entity/index'
import { CONFIG } from '@/config'
import { logger } from '@/server/logger'
import { Migration } from '@entity/Migration'
import { LogError } from '@/server/LogError'
const DataSource = new DBDataSource({
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
entities,
synchronize: false,
logging: true,
logger: new FileLogger('all', {
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
}),
extra: {
charset: 'utf8mb4_unicode_ci',
},
})
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Connection {
// eslint-disable-next-line no-use-before-define
private static instance: Connection
private connection: DBDataSource
export const getDataSource = () => DataSource
/**
* 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() {
this.connection = new DBDataSource({
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
entities,
synchronize: false,
logging: true,
logger: new FileLogger('all', {
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
}),
extra: {
charset: 'utf8mb4_unicode_ci',
},
})
}
/**
* 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(): Connection {
if (!Connection.instance) {
Connection.instance = new Connection()
}
return Connection.instance
}
public getDataSource(): DBDataSource {
console.log('production getDataSource called!')
return this.connection
}
public async init(): Promise<void> {
await this.connection.initialize()
try {
await Connection.getInstance()
} catch (error) {
// try and catch for logging
logger.fatal(`Couldn't open connection to database!`)
throw error
}
// check for correct database version
await this.checkDBVersion(CONFIG.DB_VERSION)
}
async checkDBVersion(DB_VERSION: string): Promise<void> {
const dbVersion = await Migration.findOneOrFail({ order: { version: 'DESC' } })
// return dbVersion ? dbVersion.fileName : null
if (!dbVersion.fileName.includes(DB_VERSION)) {
throw new LogError(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion ?? 'None'
}`,
)
}
}
}
export const getDataSource = () => Connection.getInstance().getDataSource()

View File

@ -76,9 +76,10 @@ export class Transaction extends BaseEntity {
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
accountBalanceCreatedAt: Decimal
accountBalanceCreatedAt?: Decimal
@Column({ type: 'tinyint' })
type: number
@ -95,26 +96,25 @@ export class Transaction extends BaseEntity {
@Column({ name: 'protocol_version', type: 'varchar', length: 255, default: '1' })
protocolVersion: string
@Column({ type: 'bigint' })
nr: number
@Column({ type: 'bigint', nullable: true })
nr?: number
@Column({ name: 'running_hash', type: 'binary', length: 48 })
runningHash: Buffer
@Column({ name: 'running_hash', type: 'binary', length: 48, nullable: true })
runningHash?: Buffer
@Column({
name: 'account_balance',
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
default: 0,
nullable: true,
transformer: DecimalTransformer,
})
accountBalanceConfirmedAt: Decimal
accountBalanceConfirmedAt?: Decimal
@Column({ name: 'iota_milestone', type: 'bigint', nullable: true })
iotaMilestone?: number
@Column({ name: 'confirmed_at', type: 'datetime' })
confirmedAt: Date
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
confirmedAt?: Date
}

View File

@ -19,25 +19,25 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
await queryFn(
`CREATE TABLE \`transactions\` (
\`id\` bigint unsigned NOT NULL AUTO_INCREMENT,
\`iota_message_id\` varbinary(32) DEFAULT NULL,
\`backend_transaction_id\` bigint unsigned DEFAULT NULL,
\`paring_transaction_id\` bigint unsigned DEFAULT NULL,
\`signing_account_id\` int unsigned DEFAULT NULL,
\`recipient_account_id\` int unsigned DEFAULT NULL,
\`iota_message_id\` varbinary(32) NULL DEFAULT NULL,
\`backend_transaction_id\` bigint unsigned NULL DEFAULT NULL,
\`paring_transaction_id\` bigint unsigned NULL DEFAULT NULL,
\`signing_account_id\` int unsigned NULL DEFAULT NULL,
\`recipient_account_id\` int unsigned NULL DEFAULT NULL,
\`sender_community_id\` int unsigned NOT NULL,
\`recipient_community_id\` int unsigned DEFAULT NULL,
\`amount\` decimal(40, 20) DEFAULT NULL,
\`recipient_community_id\` int unsigned NULL DEFAULT NULL,
\`amount\` decimal(40, 20) NULL DEFAULT NULL,
\`account_balance_created_at\` decimal(40, 20) NOT NULL,
\`type\` tinyint NOT NULL,
\`created_at\` datetime(3) NOT NULL,
\`body_bytes\` blob NOT NULL,
\`signature\` varbinary(64) NOT NULL,
\`protocol_version\` varchar(255) NOT NULL DEFAULT '1',
\`nr\` bigint NOT NULL,
\`running_hash\` varbinary(48) NOT NULL,
\`account_balance\` decimal(40, 20) NOT NULL DEFAULT 0.00000000000000000000,
\`iota_milestone\` bigint DEFAULT NULL,
\`confirmed_at\` datetime NOT NULL,
\`nr\` bigint NULL DEFAULT NULL,
\`running_hash\` varbinary(48) NULL DEFAULT NULL,
\`account_balance\` decimal(40, 20) NULL DEFAULT 0.00000000000000000000,
\`iota_milestone\` bigint NULL DEFAULT NULL,
\`confirmed_at\` datetime NULL DEFAULT NULL,
PRIMARY KEY (\`id\`),
UNIQUE KEY \`signature\` (\`signature\`),
FOREIGN KEY (\`signing_account_id\`) REFERENCES accounts(id),