From 4699e5265b5b34e48f4caaacb6fc113e2f2d206e Mon Sep 17 00:00:00 2001 From: Willy Bruns Date: Wed, 28 Sep 2016 21:23:25 -0700 Subject: [PATCH] Add tests for Payment History CSV export util (and fix bugs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes and miscellaneous: - add example Ledger transaction data: `test/unit/lib/exampleLedgerData.js` - fix how `ledgerExportUtil.js#transactionsToCSVDataURL` handled empty input - use es6 object property notation in `ledgerExportUtil.js` (re: https://github.com/brave/browser-laptop/pull/4346#discussion_r81068743) Tests (re: https://github.com/brave/browser-laptop/pull/4346#discussion_r81068457): ``` example transaction data ("exampleLedgerData.js") ✓ there should be example transactions to test with ✓ the first example transaction should have a "ballots" object ✓ the first example transaction's "ballots" object should contain >= 1 publisher / vote-count KV pairs transactionsToCSVDataURL ✓ returns a properly formatted data URL string with expected content-type (text/csv) ✓ for empty input, returns a CSV data URL containing only a header row ✓ given transactions, it returns a data URL containing the expected CSV getTransactionCSVText ✓ for empty input, returns a CSV string containing only the expected header row ✓ returns a CSV with the expected header row up to variable currency column ✓ returns a CSV with the same number of columns in every row ✓ returns a CSV with expected data types for each column in every row ✓ returns same CSV for an array containing one transaction and a single transaction object ✓ given a transaction, returns a CSV containing the right number of rows: 1 header row, 1 row per publisher, and if addTotalRow===true, 1 row with totals ✓ returns CSV text matching the CSV rows returned by getTransactionCSVRows ✓ when argument addTotalRow===true, there should be a total row with correct totals for each numeric column getTransactionCSVRows ✓ for empty input, returns an array containing only the expected CSV header row ✓ returns a CSV row array, where the first row is the expected header row up to the currency column (which varies) ✓ returns a CSV row array with the same number of columns in every row ✓ returns a CSV row array with expected data types for each column in every row ✓ returns the same output for a single transaction input as an array of 1 element and a plain transaction object ✓ given a transaction, returns an array containing the right number of CSV rows: 1 header row, 1 row per publisher, and if addTotalRow===true, 1 row with totals ✓ when argument addTotalRow===true, there should be a total row with correct totals for each numeric column getPublisherVoteData ✓ should return a publisher data object with 1 key per publisher ✓ the sum of the "fraction" value across all publisher entries should be 1 ✓ the sum of the "votes" value across all publisher entries should be equal to the overall "votes" entry for the transaction object given as input each publisher value ✓ should have "votes" (type number, >= 0) defined ✓ should have "fraction" (type number, >= 0) defined ✓ should have "contribution" (type object) defined each publisher->contribution entry ✓ should have "satoshis" (type number, >= 0) defined ✓ should have "fiat" (type number, >= 0) defined ✓ should have "currency" (type string) defined getTransactionsByViewingIds ✓ given a single viewingId as a string, it returns an array containing just that transaction (if it exists) ✓ given viewingIds as an array, it filters a transactions array for those transactions getTotalContribution ✓ returns a total contribution object total contribution object ✓ has a key "satoshis" with value of type number (>= 0) ✓ has a key "fiat" associated with an object containing two subkeys, "amount" (number) and "currency" (string) ✓ has a key, fee with value of type number (>= 0) ``` Auditor: @diracdeltas --- js/lib/ledgerExportUtil.js | 24 +- test/unit/lib/exampleLedgerData.js | 3 + test/unit/lib/ledgerExportUtilTest.js | 463 ++++++++++++++++++++++++++ 3 files changed, 480 insertions(+), 10 deletions(-) create mode 100644 test/unit/lib/exampleLedgerData.js create mode 100644 test/unit/lib/ledgerExportUtilTest.js diff --git a/js/lib/ledgerExportUtil.js b/js/lib/ledgerExportUtil.js index af4040b9180..6625b7206e1 100644 --- a/js/lib/ledgerExportUtil.js +++ b/js/lib/ledgerExportUtil.js @@ -201,7 +201,7 @@ let getPublisherVoteData = function getPublisherVoteData (transactions, viewingI * * @param {Object[]} transactions - array of transactions * @param {string[]=} viewingIds - OPTIONAL array/string with one or more viewingIds to filter transactions by (if empty, uses all tx) - * @param (boolean=) addTotalRow - OPTIONAL boolean indicating whether to add a TOTALS row (defaults false) + * @param {boolean=} addTotalRow - OPTIONAL boolean indicating whether to add a TOTALS row (defaults false) **/ let getTransactionCSVRows = function (transactions, viewingIds, addTotalRow) { let txContribData = getPublisherVoteData(transactions, viewingIds) @@ -213,7 +213,8 @@ let getTransactionCSVRows = function (transactions, viewingIds, addTotalRow) { return (a && typeof a === 'string' ? a : '').localeCompare(b && typeof b === 'string' ? b : '') }) - var currency = txContribData[publishers[0]].contribution.currency + var currency = (publishers.length ? txContribData[publishers[0]].contribution.currency : 'USD') + var headerRow = ['Publisher', 'Votes', 'Fraction', 'BTC', currency].join(',') var totalsRow = { @@ -248,7 +249,8 @@ let getTransactionCSVRows = function (transactions, viewingIds, addTotalRow) { ].join(',') })) - if (addTotalRow) { + // note: do NOT add a total row if only header row is present (no data case) + if (addTotalRow && rows.length > 1) { rows.push([ totalsRow.label, totalsRow.votes, @@ -269,17 +271,19 @@ let getTransactionCSVRows = function (transactions, viewingIds, addTotalRow) { * * @param {Object[]} transactions - array of transactions * @param {string[]=} viewingIds - OPTIONAL array/string with one or more viewingIds to filter transactions by (if empty, uses all tx) - * @param (boolean=) addTotalRow - OPTIONAL boolean indicating whether to add a TOTALS row (defaults false) + * @param {boolean=} addTotalRow - OPTIONAL boolean indicating whether to add a TOTALS row (defaults false) + * + * returns a CSV with only a header row if input is empty or invalid **/ let getTransactionCSVText = function (transactions, viewingIds, addTotalRow) { return getTransactionCSVRows(transactions, viewingIds, addTotalRow).join('\n') } module.exports = { - transactionsToCSVDataURL: transactionsToCSVDataURL, - getTransactionCSVText: getTransactionCSVText, - getTransactionCSVRows: getTransactionCSVRows, - getPublisherVoteData: getPublisherVoteData, - getTransactionsByViewingIds: getTransactionsByViewingIds, - getTotalContribution: getTotalContribution + transactionsToCSVDataURL, + getTransactionCSVText, + getTransactionCSVRows, + getPublisherVoteData, + getTransactionsByViewingIds, + getTotalContribution } diff --git a/test/unit/lib/exampleLedgerData.js b/test/unit/lib/exampleLedgerData.js new file mode 100644 index 00000000000..ad753656dc6 --- /dev/null +++ b/test/unit/lib/exampleLedgerData.js @@ -0,0 +1,3 @@ +module.exports.transactions = JSON.parse( + '{"transactions": [{"viewingId": "0ef3a02d-ffdd-41f1-a074-7a7eb1e8c332","surveyorId": "DQfCj8PHdIEJOZp9/L+FZcozgvYoIVSjPSdwqRYQDr0","contribution": {"fiat": {"amount": 5,"currency": "USD"},"rates": {"USD": 607.7},"satoshis": 813916,"fee": 8858},"submissionStamp": 1474681472637,"submissionDate": "2016-09-24T01:44:32.637Z","submissionId": "a5c64f5d310f67f5d0fa5b7d6e56daad9cc146a161b9cfa93428cc7979decd73","credential": "{\\"userId\\":\\"0ef3a02dffdd1f1a0747a7eb1e8c332\\",\\"registrarVK\\":\\"==========ANONLOGIN_VK_BEG==========4Wx4HlyvjCQ6wAVFJ/gJMzwvQJwwGDkbqIzhejDeD3f 1Gf/dWEBkiVPUjM1iocgSvFaHmKtNhQDlKeEOZk89Mt 1SgUQuiExpbxkkLNXiwnCijrRF52cid48H24Vw0PuB4 2S0A4mTIJB4wGiGDhpM8kJvxwzuJYlocBBXeiy7IPPS 1A61IVdw52utSEy83T6BUJb+MMTxx5fyObs5hco1qTWW B6xG+Xk0SHD9Z9N17gEFK/IaAaqulOXEUG92UTXeOrl 13axEl6uk6jgVBqY/N8Uk9Ln8bImq2sI5qfI68FyqCsE 1KGfWuEoPnK7DuvE5rA37iNTf1n+87oMnuBoGxCG8zY 1kk+mDkPBS+Ixpm9HIsX6MVlWUv7CiM8fhrpFpwUwUQ 1aAo0g9PxObLWbWL39Jd/kBv4FXRJ19Q6wb2/9DuP4k 30Xa/N/vUJJYX3JGODvdb9pzVx6bC5ZgZwwCYGUP23Y 3oKCelicy9ueqQe6cMLFCg9QWKyBeSGY4VXh366GJiF 1 0===========ANONLOGIN_VK_END==========\\",\\"masterUserToken\\":\\"==========ANONLOGIN_CRED_BEG==========0ef3a02dffdd1f1a0747a7eb1e8c3325wnnP2Iw9BB2LnxU2R5Lfi5RPbuwPSueUPw5K/bGvQ/2gYJKd1SzCu+4ZP3xkTn2nci4PrO00SYnXtprKoXhV4 UR3uL9lzwC4KAn9yxWFmfANuhRHvSYWGJ4HbMJTWGG 19o2wiMZmPu5Ot4y9+6hIL9tVm1GuuNVllOga4UpdZTD4hrll72x9bp/opVAH6Nr1pdpHv+vMY8S28j+NfcXd9M===========ANONLOGIN_CRED_END==========\\"}","surveyorIds": ["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"],"count": 43,"satoshis": 813916,"votes": 43,"ballots": {"instructables.com": 1,"karelzak.blogspot.com": 22,"sourceforge.net": 2,"slate.com": 6,"coindesk.com": 3,"kernel.org": 2,"bbc.com": 1,"archlinux.org": 1,"waitbutwhy.com": 3,"chronicle.com": 2}}]}' +).transactions diff --git a/test/unit/lib/ledgerExportUtilTest.js b/test/unit/lib/ledgerExportUtilTest.js new file mode 100644 index 00000000000..bd2c3989305 --- /dev/null +++ b/test/unit/lib/ledgerExportUtilTest.js @@ -0,0 +1,463 @@ +/* global describe, it, before */ +const assert = require('assert') +const underscore = require('underscore') + +require('../braveUnit') + +const ledgerExportUtil = require('../../../js/lib/ledgerExportUtil.js') +const base64Encode = require('../../../js/lib/base64').encode + +const CSV_HEADER_ROW_PREFIX_COLUMNS = ['Publisher', 'Votes', 'Fraction', 'BTC'] +const CSV_HEADER_ROW_PREFIX = CSV_HEADER_ROW_PREFIX_COLUMNS.join(',') +const DEFAULT_CSV_HEADER_ROW_COLUMNS = CSV_HEADER_ROW_PREFIX_COLUMNS.concat(['USD']) +const DEFAULT_CSV_HEADER_ROW = DEFAULT_CSV_HEADER_ROW_COLUMNS.join(',') +const CSV_COLUMN_COUNT = DEFAULT_CSV_HEADER_ROW.split(',').length +const EMPTY_CSV = DEFAULT_CSV_HEADER_ROW +// N.B. the expected datatype for the 'USD'/fiat column is a 'string' because it is of form '5.00 USD' +const CSV_EXPECTED_COLUMN_DATATYPES = ['string', 'number', 'number', 'number', 'string'] + +const CSV_CONTENT_TYPE = 'text/csv' +const CSV_DATA_URI_PREFIX = 'data:' + CSV_CONTENT_TYPE + ';base64,' +const EMPTY_CSV_DATA_URL = CSV_DATA_URI_PREFIX + base64Encode(EMPTY_CSV) + +const exampleTransactions = require('./exampleLedgerData').transactions +const exampleTransaction = exampleTransactions[0] + +const PUBLISHERS = (exampleTransaction.ballots ? underscore.keys(exampleTransaction.ballots) : []) +const NUM_PUBLISHERS = PUBLISHERS.length + +describe('ledger export utilities test', function () { + describe('example transaction data ("exampleLedgerData.js")', function () { + it('there should be example transactions to test with', function () { + assert(!!exampleTransactions) + assert(exampleTransactions.length) + }) + + it('the first example transaction should have a "ballots" object', function () { + assert(!!exampleTransaction.ballots) + }) + + it('the first example transaction\'s "ballots" object should contain >= 1 publisher / vote-count KV pairs', function () { + assert(NUM_PUBLISHERS > 0) + assert.equal(typeof PUBLISHERS[0], 'string') + assert.equal(typeof exampleTransaction.ballots[PUBLISHERS[0]], 'number') + }) + }) + + describe('transactionsToCSVDataURL', function () { + it(`returns a properly formatted data URL string with expected content-type (${CSV_CONTENT_TYPE})`, function () { + let output = ledgerExportUtil.transactionsToCSVDataURL(exampleTransactions) + + assert.equal(!!output, true) + assert.equal(typeof output, 'string') + assert(output.length > CSV_DATA_URI_PREFIX.length) + assert.equal(output.slice(0, 'data:'.length), 'data:') + assert.equal(output.slice('data:'.length, 'data:'.length + CSV_CONTENT_TYPE.length), CSV_CONTENT_TYPE) + assert.equal(output.slice('data:'.length + CSV_CONTENT_TYPE.length, 'data:'.length + CSV_CONTENT_TYPE.length + ';base64,'.length), ';base64,') + }) + + it('for empty input, returns a CSV data URL containing only a header row', function () { + let output + + output = ledgerExportUtil.transactionsToCSVDataURL(undefined) + assert.equal(output, EMPTY_CSV_DATA_URL) + + output = ledgerExportUtil.transactionsToCSVDataURL(null) + assert.equal(output, EMPTY_CSV_DATA_URL) + + output = ledgerExportUtil.transactionsToCSVDataURL([]) + assert.equal(output, EMPTY_CSV_DATA_URL) + }) + + it('given transactions, it returns a data URL containing the expected CSV', function () { + const ADD_TOTAL_ROW = true + const EXPECTED_CSV_TEXT = ledgerExportUtil.getTransactionCSVText(exampleTransactions, null, ADD_TOTAL_ROW) + + let output = ledgerExportUtil.transactionsToCSVDataURL(exampleTransactions) + + assert.equal(output, CSV_DATA_URI_PREFIX + base64Encode(EXPECTED_CSV_TEXT)) + }) + }) + + describe('getTransactionCSVText', function () { + it('for empty input, returns a CSV string containing only the expected header row', function () { + let output + + output = ledgerExportUtil.getTransactionCSVText(undefined) + assert.equal(output, EMPTY_CSV) + + output = ledgerExportUtil.getTransactionCSVText(null) + assert.equal(output, EMPTY_CSV) + + output = ledgerExportUtil.getTransactionCSVText([]) + assert.equal(output, EMPTY_CSV) + }) + + it('returns a CSV with the expected header row up to variable currency column', function () { + let output = ledgerExportUtil.getTransactionCSVText(exampleTransactions) + assert(!!output, 'expected CsV output to exist') + + let rows = output.split('\n') + + checkHeaderRowPrefixForRows(rows) + }) + + it('returns a CSV with the same number of columns in every row', function () { + let output = ledgerExportUtil.getTransactionCSVText(exampleTransactions) + + let rows = output.split('\n') + + checkColumnCountsForRows(rows) + }) + + it('returns a CSV with expected data types for each column in every row', function () { + let ADD_TOTAL_ROW = true + let output = ledgerExportUtil.getTransactionCSVText(exampleTransactions, null, ADD_TOTAL_ROW) + + let rows = output.split('\n') + + checkColumnDatatypesForRows(rows) + }) + + it('returns same CSV for an array containing one transaction and a single transaction object', function () { + let singleTxOutput = ledgerExportUtil.getTransactionCSVText(exampleTransaction) + let arrayWithSingleTxOutput = ledgerExportUtil.getTransactionCSVText(exampleTransactions) + + assert.equal(singleTxOutput, arrayWithSingleTxOutput) + }) + + it('given a transaction, returns a CSV containing the right number of rows: 1 header row, 1 row per publisher, and if addTotalRow===true, 1 row with totals', function () { + const EXPECTED_CSV_ROW_COUNT_NO_TOTAL = 1 + NUM_PUBLISHERS + const EXPECTED_CSV_ROW_COUNT_WITH_TOTAL = 1 + NUM_PUBLISHERS + 1 + + // output with NO TOTAL ROW + var output = ledgerExportUtil.getTransactionCSVText(exampleTransaction) + + var rows = output.split('\n') + assert.equal(rows.length, EXPECTED_CSV_ROW_COUNT_NO_TOTAL) + + let ADD_TOTAL_ROW = true + output = ledgerExportUtil.getTransactionCSVText(exampleTransaction, null, ADD_TOTAL_ROW) + rows = output.split('\n') + assert.equal(rows.length, EXPECTED_CSV_ROW_COUNT_WITH_TOTAL) + }) + + it('returns CSV text matching the CSV rows returned by getTransactionCSVRows', function () { + let ADD_TOTAL_ROW = true + let output = ledgerExportUtil.getTransactionCSVText(exampleTransactions, null, ADD_TOTAL_ROW) + + let rows = output.split('\n') + + let expectedOutputRows = ledgerExportUtil.getTransactionCSVRows(exampleTransactions, null, ADD_TOTAL_ROW) + assert.deepEqual(rows, expectedOutputRows) + }) + + it('when argument addTotalRow===true, there should be a total row with correct totals for each numeric column', function () { + let ADD_TOTAL_ROW = true + let output = ledgerExportUtil.getTransactionCSVText(exampleTransaction, null, ADD_TOTAL_ROW) + + let rows = output.split('\n') + + checkTotalRow(rows) + }) + }) + + describe('getTransactionCSVRows', function () { + it('for empty input, returns an array containing only the expected CSV header row', function () { + let output + + output = ledgerExportUtil.getTransactionCSVRows(undefined) + assert.deepEqual(output, [DEFAULT_CSV_HEADER_ROW]) + + output = ledgerExportUtil.getTransactionCSVRows(null) + assert.deepEqual(output, [DEFAULT_CSV_HEADER_ROW]) + + output = ledgerExportUtil.getTransactionCSVRows([]) + assert.deepEqual(output, [DEFAULT_CSV_HEADER_ROW]) + }) + + it('returns a CSV row array, where the first row is the expected header row up to the currency column (which varies)', function () { + let rows = ledgerExportUtil.getTransactionCSVRows(exampleTransactions) + + checkHeaderRowPrefixForRows(rows) + }) + + it('returns a CSV row array with the same number of columns in every row', function () { + let rows = ledgerExportUtil.getTransactionCSVRows(exampleTransactions) + + checkColumnCountsForRows(rows) + }) + + it('returns a CSV row array with expected data types for each column in every row', function () { + let rows = ledgerExportUtil.getTransactionCSVRows(exampleTransactions) + + checkColumnDatatypesForRows(rows) + }) + + it('returns the same output for a single transaction input as an array of 1 element and a plain transaction object', function () { + let singleTxRows = ledgerExportUtil.getTransactionCSVText(exampleTransaction) + let arrayWithSingleTxRows = ledgerExportUtil.getTransactionCSVText(exampleTransactions) + + assert.deepEqual(singleTxRows, arrayWithSingleTxRows) + }) + + it('given a transaction, returns an array containing the right number of CSV rows: 1 header row, 1 row per publisher, and if addTotalRow===true, 1 row with totals', function () { + const EXPECTED_CSV_ROW_COUNT_NO_TOTAL = 1 + NUM_PUBLISHERS + const EXPECTED_CSV_ROW_COUNT_WITH_TOTAL = 1 + NUM_PUBLISHERS + 1 + + // output with NO TOTAL ROW + var rows = ledgerExportUtil.getTransactionCSVRows(exampleTransaction) + + assert.equal(rows.length, EXPECTED_CSV_ROW_COUNT_NO_TOTAL) + + let ADD_TOTAL_ROW = true + rows = ledgerExportUtil.getTransactionCSVRows(exampleTransaction, null, ADD_TOTAL_ROW) + + assert.equal(rows.length, EXPECTED_CSV_ROW_COUNT_WITH_TOTAL) + }) + + it('when argument addTotalRow===true, there should be a total row with correct totals for each numeric column', function () { + let ADD_TOTAL_ROW = true + let rows = ledgerExportUtil.getTransactionCSVRows(exampleTransaction, null, ADD_TOTAL_ROW) + + checkTotalRow(rows) + }) + }) + + describe('getPublisherVoteData', function () { + let publisherData + let publishers + + before(function () { + publisherData = ledgerExportUtil.getPublisherVoteData(exampleTransactions) + if (publisherData && typeof publisherData === 'object') { + publishers = underscore.keys(publisherData) + } + }) + + it('should return a publisher data object with 1 key per publisher', function () { + assert(!!publisherData, 'returned publisher data should exist') + assert.equal(typeof publisherData, 'object', 'returned publisher data should be an object') + + let publishers = underscore.keys(publisherData) + + assert(!!publishers && underscore.isArray(publishers)) + + assert.equal(publishers.length, NUM_PUBLISHERS, 'there should be 1 key per publisher') + + publishers.forEach(function (publisher) { + let value = publisherData[publisher] + assert(!!value, `publisher "${publisher}" does not have a value associated with it`) + assert.equal(typeof value, 'object', `publisher "${publisher}" has a value that is not an object associated with it (value: "${value}")`) + }) + }) + + describe('each publisher value', function () { + it('should have "votes" (type number, >= 0) defined', function () { + publishers.forEach(function (publisher) { + assert(typeof publisherData[publisher].votes, 'number') + assert(publisherData[publisher].votes >= 0) + }) + }) + + it('should have "fraction" (type number, >= 0) defined', function () { + publishers.forEach(function (publisher) { + assert(typeof publisherData[publisher].fraction, 'number') + assert(publisherData[publisher].fraction >= 0) + }) + }) + + it('should have "contribution" (type object) defined', function () { + publishers.forEach(function (publisher) { + assert(typeof publisherData[publisher].contribution, 'object') + }) + }) + + describe('each publisher->contribution entry', function () { + it('should have "satoshis" (type number, >= 0) defined', function () { + publishers.forEach(function (publisher) { + let publisherContributionEntry = publisherData[publisher].contribution + assert(typeof publisherContributionEntry.satoshis, 'number') + assert(publisherContributionEntry.satoshis >= 0) + }) + }) + + it('should have "fiat" (type number, >= 0) defined', function () { + publishers.forEach(function (publisher) { + let publisherContributionEntry = publisherData[publisher].contribution + assert(typeof publisherContributionEntry.fiat, 'number') + assert(publisherContributionEntry.fiat >= 0) + }) + }) + + it('should have "currency" (type string) defined', function () { + publishers.forEach(function (publisher) { + let publisherContributionEntry = publisherData[publisher].contribution + assert(typeof publisherContributionEntry.currency, 'string') + }) + }) + }) + }) + + it('the sum of the "fraction" value across all publisher entries should be 1', function () { + let fractionSum = 0 + publishers.forEach(function (publisher) { + fractionSum += publisherData[publisher].fraction + }) + assert.equal(fractionSum, 1) + }) + + it('the sum of the "votes" value across all publisher entries should be equal to the overall "votes" entry for the transaction object given as input', function () { + let txLevelVotesSum = 0 + exampleTransactions.forEach(function (tx) { + txLevelVotesSum += tx.votes + }) + let votesSum = 0 + publishers.forEach(function (publisher) { + votesSum += publisherData[publisher].votes + }) + assert.equal(votesSum, txLevelVotesSum) + }) + }) + + describe('getTransactionsByViewingIds', function () { + it('given a single viewingId as a string, it returns an array containing just that transaction (if it exists)', function () { + const EXAMPLE_VIEWING_ID = exampleTransactions[0].viewingId + let filteredTxArr = ledgerExportUtil.getTransactionsByViewingIds(exampleTransactions, EXAMPLE_VIEWING_ID) + + assert(underscore.isArray(filteredTxArr), 'it should return an array') + assert.equal(filteredTxArr.length, 1, 'it should contain a single transaction when a viewingId present in original array is provided') + assert.deepEqual(filteredTxArr[0], exampleTransactions[0], 'it should return the correct transaction object from the array') + + let emptyFilteredTxArr = ledgerExportUtil.getTransactionsByViewingIds(exampleTransactions, 'INVALID VIEWING ID') + assert(underscore.isArray(emptyFilteredTxArr), 'it should return an array') + assert.equal(emptyFilteredTxArr.length, 0, 'it should be an empty array when a viewingId NOT present in original array is provided') + }) + + it('given viewingIds as an array, it filters a transactions array for those transactions', function () { + // TODO: NEED MORE TRANSACTIONS IN EXAMPLE DATA TO REALLY MAKE THIS A GOOD TEST + const EXAMPLE_VIEWING_IDS = [exampleTransactions[0].viewingId, 'INVALID VIEWING ID EXAMPLE'] + let filteredTxArr = ledgerExportUtil.getTransactionsByViewingIds(exampleTransactions, EXAMPLE_VIEWING_IDS) + + assert(underscore.isArray(filteredTxArr), 'it should return an array') + assert.equal(filteredTxArr.length, 1, 'the returned array should contain only transactions with matching viewingIds') + assert.deepEqual(filteredTxArr[0], exampleTransactions[0], 'it should return the correct transaction object from the array') + }) + }) + + describe('getTotalContribution', function () { +/** + var totalContribution = { + satoshis: 0, + fiat: { amount: 0, currency: null }, + fee: 0 + } +**/ + let contributionData + + before(function () { + contributionData = ledgerExportUtil.getTotalContribution(exampleTransactions) + }) + + it('returns a total contribution object', function () { + assert.equal(typeof contributionData, 'object') + }) + + describe('total contribution object', function () { + it('has a key "satoshis" with value of type number (>= 0)', function () { + assert.equal(typeof contributionData.satoshis, 'number') + assert(contributionData.satoshis >= 0) + }) + + it('has a key "fiat" associated with an object containing two subkeys, "amount" (number) and "currency" (string)', function () { + assert.equal(typeof contributionData.fiat, 'object', 'should have a key "fiat" with an object associated') + assert.equal(typeof contributionData.fiat.amount, 'number', 'should have a key "amount" with value of type "number"') + assert.equal(typeof contributionData.fiat.currency, 'string', 'should have a key "amount" with value of type "string"') + }) + + it('has a key, fee with value of type number (>= 0)', function () { + assert.equal(typeof contributionData.fee, 'number') + assert(contributionData.fee >= 0) + }) + }) + }) +}) + +function checkColumnCountsForRows (rows) { + for (var rowIdx = 0; rowIdx < rows.length; rowIdx++) { + let row = rows[rowIdx] + assert(!!row, `expected row ${rowIdx} to exist`) + + let cols = row.split(',') + assert.equal(cols.length, CSV_COLUMN_COUNT, `expected row ${rowIdx} to have ${CSV_COLUMN_COUNT} columns`) + } +} + +function checkHeaderRowPrefixForRows (rows) { + assert(!!rows && rows.length, 'expected output to have at least one row') + + let headerRow = rows[0] + assert(!!headerRow, 'expected header row to exist') + assert.equal(headerRow.slice(0, CSV_HEADER_ROW_PREFIX.length), CSV_HEADER_ROW_PREFIX) +} + +function checkColumnDatatypesForRows (rows) { + let COLUMN_LABELS = CSV_HEADER_ROW_PREFIX_COLUMNS.concat(['FIAT']) + // start at rowIdx = 1 to SKIP the header row + for (var rowIdx = 1; rowIdx < rows.length; rowIdx++) { + let row = rows[rowIdx] + assert(!!row, `expected row ${rowIdx} to exist`) + let cols = row.split(',') + for (var colIdx = 0; colIdx < cols.length; colIdx++) { + let colVal = cols[colIdx] + + if (CSV_EXPECTED_COLUMN_DATATYPES[colIdx] === 'number' && + `${parseFloat(colVal)}` === colVal) { + colVal = parseFloat(colVal) + } + + let columnDatatype = typeof colVal + assert.equal(columnDatatype, CSV_EXPECTED_COLUMN_DATATYPES[colIdx], `expected ${COLUMN_LABELS[colIdx]} column (value = ${cols[colIdx]}) for row ${rowIdx} to be type "${CSV_EXPECTED_COLUMN_DATATYPES[colIdx]}" , but found type "${columnDatatype}"`) + } + } +} + +function checkTotalRow (rows) { + let totalRow = rows[rows.length - 1] + + let totalRowColumns = totalRow.split(',') + + for (var colIdx = 0; colIdx < totalRowColumns.length; colIdx++) { + let expectedColType = CSV_EXPECTED_COLUMN_DATATYPES[colIdx] + + if (expectedColType === 'number') { + let totalRowValue = parseFloat(totalRowColumns[colIdx]) + checkCSVColumnTotal(rows.slice(1, rows.length - 1), colIdx, totalRowValue) + } + } +} + +function checkCSVColumnTotal (rows, colIdx, expectedTotal) { + let sum = 0 + + for (var rowIdx = 0; rowIdx < rows.length; rowIdx++) { + let row = rows[rowIdx] + let cols = row.split(',') + + let colVal = (cols || [])[colIdx] + assert(colVal === 0 || (!!colVal), `value for row ${rowIdx}, column ${colIdx} is not defined!`) + + let parsedColVal = parseFloat(colVal) + assert(`${parsedColVal}` === colVal, `value (${colVal}) for row ${rowIdx}, column ${colIdx} is not numeric`) + + sum += parsedColVal + } + + assert.equal(sum, expectedTotal, `Sum for column ${colIdx} across ${rows.length} rows does not match expected value`) + + /** + * anyone debugging this test may want to temporarily uncomment the following two ines: + * let columnNames = CSV_HEADER_ROW_PREFIX_COLUMNS.concat(['Fiat']) + * console.log(`\tSum for column ${columnNames[colIdx]} (#${colIdx}) across ${rows.length} rows (=${sum}) matches expected total (${expectedTotal})`) + **/ +}