Skip to content

Commit

Permalink
chore(ref-imp): more tests to cover branches (#987)
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacJChen committed Dec 18, 2020
1 parent c2d086a commit fa7a4fd
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 7 deletions.
6 changes: 4 additions & 2 deletions lib/bitcoin/BitcoinProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,10 @@ export default class BitcoinProcessor {

this.mongoDbLockTransactionStore = new MongoDbLockTransactionStore(config.mongoDbConnectionString, config.databaseName);

const valueTimeLockTransactionFeesInBtc = config.valueTimeLockTransactionFeesAmountInBitcoins === 0 ? 0
: config.valueTimeLockTransactionFeesAmountInBitcoins || 0.25;
// TODO: #988 Can potentially remove the default. If removed, the config will be required and more explicit but user can set bad values (0).
// https://github.com/decentralized-identity/sidetree/issues/988
const valueTimeLockTransactionFeesInBtc = config.valueTimeLockTransactionFeesAmountInBitcoins === undefined ? 0.25
: config.valueTimeLockTransactionFeesAmountInBitcoins;

this.lockMonitor = new LockMonitor(
this.bitcoinClient,
Expand Down
72 changes: 67 additions & 5 deletions tests/bitcoin/BitcoinClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ describe('BitcoinClient', async () => {
const actual = new BitcoinClient('uri:mock', 'u', 'p', ctorWallet, 10, 10, 10, expectedEstimatedFee);
expect(actual['estimatedFeeSatoshiPerKB']).toEqual(expectedEstimatedFee);
});

it('should construct without authorization if username and passwords are not supplied', () => {
const expectedEstimatedFee = 42;
const actual = new BitcoinClient('uri:mock', undefined, undefined, ctorWallet, 10, 10, 10, expectedEstimatedFee);
expect(actual['bitcoinAuthorization']).toEqual(undefined);
});
});

describe('createSidetreeTransaction', () => {
Expand Down Expand Up @@ -604,11 +610,7 @@ describe('BitcoinClient', async () => {

describe('createBitcoinOutputModel', () => {
it('should work if the output does not have any script', async (done) => {
const mockOutput = new Transaction.Output({
script: Script.empty(),
satoshis: 10
});

const mockOutput = { script: undefined, saotshis: 123 } as any;
const actual = BitcoinClient['createBitcoinOutputModel'](mockOutput);
expect(actual.scriptAsmAsString).toEqual('');
expect(actual.satoshis).toEqual(mockOutput.satoshis);
Expand Down Expand Up @@ -1100,6 +1102,44 @@ describe('BitcoinClient', async () => {
done();
});

it('should call retry-fetch without authohrization', async (done) => {
const request: any = {};
const memberName = 'memberRequestName';
const memberValue = 'memberRequestValue';
request[memberName] = memberValue;
const bodyIdentifier = 12345;
const result = 'some_result';

const retryFetchSpy = spyOn(bitcoinClient as any, 'fetchWithRetry');
retryFetchSpy.and.callFake((uri: string, params: any) => {
expect(uri).toContain(bitcoinPeerUri);
expect(params.method).toEqual('post');
expect(JSON.parse(params.body)[memberName]).toEqual(memberValue);
return Promise.resolve({
status: httpStatus.OK,
body: bodyIdentifier
});
});
const readUtilSpy = spyOn(ReadableStream, 'readAll').and.callFake((body: any) => {
expect(body).toEqual(bodyIdentifier);
return Promise.resolve(Buffer.from(JSON.stringify({
result,
error: null,
id: null
})));
});

const originalAuthorization = (bitcoinClient as any).bitcoinAuthorization;
(bitcoinClient as any).bitcoinAuthorization = undefined;

const actual = await bitcoinClient['rpcCall'](request, true);
expect(actual).toEqual(result);
expect(retryFetchSpy).toHaveBeenCalled();
expect(readUtilSpy).toHaveBeenCalled();
(bitcoinClient as any).bitcoinAuthorization = originalAuthorization;
done();
});

it('should throw if the request failed', async (done) => {
const request: any = {
test: 'some random string'
Expand Down Expand Up @@ -1197,6 +1237,28 @@ describe('BitcoinClient', async () => {
done();
});

it('should fetch the URI with the given requestParameters without timeout', async (done) => {
const path = 'http://some_random_path';
const request: any = {
headers: {}
};
const memberName = 'headerMember';
const memberValue = 'headerValue';
request.headers[memberName] = memberValue;
const result = 200;

fetchSpy.and.callFake((uri: string, params: any) => {
expect(uri).toEqual(path);
expect(params.headers[memberName]).toEqual(memberValue);
return Promise.resolve(result);
});

const actual = await bitcoinClient['fetchWithRetry'](path, request, false);
expect(actual as any).toEqual(result);
expect(fetchSpy).toHaveBeenCalled();
done();
});

it('should retry with an extended time period if the request timed out', async (done) => {
const requestId = 'someRequestId';
let timeout: number;
Expand Down
63 changes: 63 additions & 0 deletions tests/bitcoin/BitcoinProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,42 @@ describe('BitcoinProcessor', () => {
expect(bitcoinProcessor['bitcoinClient']['sidetreeTransactionFeeMarkupPercentage']).toEqual(0);
expect(bitcoinProcessor['bitcoinClient']['estimatedFeeSatoshiPerKB']).toEqual(42);
});

it('should use appropriate config values with lock amount', () => {
const config: IBitcoinConfig = {
bitcoinDataDirectory: undefined,
bitcoinFeeSpendingCutoffPeriodInBlocks: 100,
bitcoinFeeSpendingCutoff: 1,
bitcoinPeerUri: randomString(),
bitcoinRpcUsername: 'admin',
bitcoinRpcPassword: 'password123',
bitcoinWalletOrImportString: BitcoinClient.generatePrivateKey('testnet'),
databaseName: randomString(),
defaultTransactionFeeInSatoshisPerKB: 42,
genesisBlockNumber: randomNumber(),
mongoDbConnectionString: randomString(),
sidetreeTransactionPrefix: randomString(4),
lowBalanceNoticeInDays: undefined,
requestTimeoutInMilliseconds: undefined,
requestMaxRetries: undefined,
transactionPollPeriodInSeconds: 60,
sidetreeTransactionFeeMarkupPercentage: 0,
valueTimeLockUpdateEnabled: true,
valueTimeLockPollPeriodInSeconds: 60,
valueTimeLockAmountInBitcoins: 1,
valueTimeLockTransactionFeesAmountInBitcoins: 123
};

const bitcoinProcessor = new BitcoinProcessor(config);
expect(bitcoinProcessor.genesisBlockNumber).toEqual(config.genesisBlockNumber);
expect(bitcoinProcessor.lowBalanceNoticeDays).toEqual(28);
expect((bitcoinProcessor as any).config.transactionPollPeriodInSeconds).toEqual(60);
expect((bitcoinProcessor as any).config.sidetreeTransactionPrefix).toEqual(config.sidetreeTransactionPrefix);
expect(bitcoinProcessor['transactionStore']['databaseName']).toEqual(config.databaseName);
expect(bitcoinProcessor['transactionStore']['serverUrl']).toEqual(config.mongoDbConnectionString);
expect(bitcoinProcessor['bitcoinClient']['sidetreeTransactionFeeMarkupPercentage']).toEqual(0);
expect(bitcoinProcessor['bitcoinClient']['estimatedFeeSatoshiPerKB']).toEqual(42);
});
});

describe('initialize', () => {
Expand Down Expand Up @@ -1220,6 +1256,33 @@ describe('BitcoinProcessor', () => {
expect(rawDataParserSpy).toHaveBeenCalled();
expect(writeBlocksToMetadataStoreWithFeeSpy).toHaveBeenCalledTimes(1);
});

it('should process transactions as intended with no raw data', async () => {
// this is the end to end test
const startBlock = randomBlock(testConfig.genesisBlockNumber);
const getCurrentHeightMock = spyOn(bitcoinProcessor['bitcoinClient'], 'getCurrentBlockHeight').and.returnValue(Promise.resolve(startBlock.height + 1));
const getCurrentHashMock = spyOn(bitcoinProcessor['bitcoinClient'], 'getBlockInfoFromHeight')
.and.returnValue(Promise.resolve({ hash: 'hash2', height: startBlock.height + 1, previousHash: 'hash1' }));
const fsReaddirSyncSpy = spyOn(fs, 'readdirSync').and.returnValue(['blk001.dat' as any]);
const fsReadFileSyncSpy = spyOn(fs, 'readFileSync').and.returnValue(Buffer.from('someBuffer'));
const writeBlocksToMetadataStoreWithFeeSpy = spyOn(bitcoinProcessor, 'writeBlocksToMetadataStoreWithFee' as any);
const rawDataParserSpy = spyOn(BitcoinRawDataParser, 'parseRawDataFile').and.returnValue([]); // make it empty on purpose
const mockFeeCalculator = {
calculateNormalizedTransactionFeeFromBlock (block: BlockMetadata) { return Math.floor(block.normalizedFee); },
async getNormalizedFee () { return 509; },
async addNormalizedFeeToBlockMetadata (block: BlockMetadataWithoutNormalizedFee) { return Object.assign({ normalizedFee: 111 }, block); }
};
spyOn(bitcoinProcessor['versionManager'], 'getFeeCalculator').and.returnValue(mockFeeCalculator);

await bitcoinProcessor['fastProcessTransactions'](startBlock);
expect(getCurrentHeightMock).toHaveBeenCalled();
expect(getCurrentHashMock).toHaveBeenCalled();
expect(fsReaddirSyncSpy).toHaveBeenCalled();
expect(fsReadFileSyncSpy).toHaveBeenCalled();
// onces because 1 block is forked
expect(rawDataParserSpy).toHaveBeenCalled();
expect(writeBlocksToMetadataStoreWithFeeSpy).toHaveBeenCalledTimes(1);
});
});

describe('processTransactions', () => {
Expand Down
9 changes: 9 additions & 0 deletions tests/bitcoin/SidetreeTransactionParser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ describe('SidetreeTransactionParser', () => {
expect(actual).not.toBeDefined();
done();
});

it('should return undefined if data does not start with sidetree prefix', async (done) => {
const sidetreeData = 'some test data';
const mockDataOutput = createValidDataOutput(sidetreeData);

const actual = sidetreeTxnParser['getSidetreeDataFromOutputIfExist'](mockDataOutput, 'notSidetree');
expect(actual).not.toBeDefined();
done();
});
});

describe('getValidWriterFromInputs', () => {
Expand Down
50 changes: 50 additions & 0 deletions tests/core/Observer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,56 @@ describe('Observer', async () => {
expect(processedTransactions[3].anchorString).toEqual('4thTransaction');
});

it('should log error if blockchian throws', async () => {
const blockchainClient = new Blockchain(config.blockchainServiceUri);

let readInvocationCount = 0;
spyOn(blockchainClient, 'read').and.callFake(() => {
readInvocationCount++;
throw new Error('Expected test error');
});
const loggerErrorSpy = spyOn(Logger, 'error').and.callThrough();

// Start the Observer.
const observer = new Observer(
versionManager,
blockchainClient,
config.maxConcurrentDownloads,
operationStore,
transactionStore,
transactionStore,
1
);

// mocking throughput limiter to make testing easier
spyOn(observer['throughputLimiter'], 'getQualifiedTransactions').and.callFake(
(transactions: TransactionModel[]) => {
return new Promise((resolve) => { resolve(transactions); });
}
);

await observer.startPeriodicProcessing(); // Asynchronously triggers Observer to start processing transactions immediately.

observer.stopPeriodicProcessing(); // Asynchronously stops Observer from processing more transactions after the initial processing cycle.

await retry(async _bail => {
if (readInvocationCount > 0) {
return;
}

// NOTE: the `retry` library retries if error is thrown.
throw new Error('Two transaction processing cycles have not occured yet.');
}, {
retries: 3,
minTimeout: 1000, // milliseconds
maxTimeout: 1000 // milliseconds
});

// throughput limiter applies logic to filter out some transactions
expect(loggerErrorSpy).toHaveBeenCalledWith('Encountered unhandled and possibly fatal Observer error, must investigate and fix:');
expect(loggerErrorSpy).toHaveBeenCalledWith(new Error('Expected test error'));
});

it('should not rollback if blockchain time in bitcoin service is behind core service.', async () => {
const anchoredData = AnchoredDataSerializer.serialize({ coreIndexFileUri: '1stTransaction', numberOfOperations: 1 });
const transaction = {
Expand Down

0 comments on commit fa7a4fd

Please sign in to comment.