diff --git a/src/lib/moduleExec.spec.ts b/src/lib/moduleExec.spec.ts index 8f2f6fd3..19cb8231 100644 --- a/src/lib/moduleExec.spec.ts +++ b/src/lib/moduleExec.spec.ts @@ -9,13 +9,59 @@ import _moduleExec from './moduleExec'; const yf = { _env, _fetch, - _opts: { validation: { logErrors: true }}, + _opts: { validation: { logErrors: true, logOptionsErrors: false }}, _moduleExec, search }; describe('moduleExec', () => { + describe('options validation', () => { + + it('throws InvalidOptions on invalid options', async () => { + const rwo = (options:any) => yf.search('symbol', options); + await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) + }); + + it('logs errors on invalid options when logOptionsErrors = true', async () => { + yf._opts.validation.logOptionsErrors = true; + const realConsole = console; + const fakeConsole = { error: jest.fn(), log: jest.fn(), dir: jest.fn() }; + + /* @ts-ignore */ + console = fakeConsole; + const rwo = (options:any) => yf.search('symbol', options); + await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) + console = realConsole; + + expect( + fakeConsole.log.mock.calls.length + + fakeConsole.error.mock.calls.length + + fakeConsole.dir.mock.calls.length + ).toBeGreaterThan(1); + yf._opts.validation.logOptionsErrors = false; + }); + + it('does not log errors on invalid options when logOptionsErrors = false', async () => { + yf._opts.validation.logOptionsErrors = false; + const realConsole = console; + const fakeConsole = { error: jest.fn(), log: jest.fn(), dir: jest.fn() }; + + /* @ts-ignore */ + console = fakeConsole; + const rwo = (options:any) => yf.search('symbol', options); + await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) + console = realConsole; + + expect( + fakeConsole.log.mock.calls.length + + fakeConsole.error.mock.calls.length + + fakeConsole.dir.mock.calls.length + ).toBe(0); + }); + + }); + describe('result validation', () => { it('throws on unexpected input', async () => { diff --git a/src/lib/moduleExec.ts b/src/lib/moduleExec.ts index 5e00d136..f8a4c0de 100644 --- a/src/lib/moduleExec.ts +++ b/src/lib/moduleExec.ts @@ -17,6 +17,7 @@ import validateAndCoerceTypes from './validateAndCoerceTypes'; import csv2json from './csv2json'; +import _opts from './options'; interface TransformFunc { // The consuming module itself will have a stricter return type. @@ -95,16 +96,24 @@ interface ModuleExecOptions { type ThisWithFetch = { [key: string]: any; _moduleExec: Function }; export default async function moduleExec(this: ThisWithFetch, opts: ModuleExecOptions) { - const query = opts.query; + const queryOpts = opts.query; const moduleOpts = opts.moduleOptions; + const moduleName = opts.moduleName; + const resultOpts = opts.result; // Check that query options passed by the user are valid for this module - validateAndCoerceTypes(query.overrides, query.schemaKey, opts.moduleName); + validateAndCoerceTypes({ + source: moduleName, + type: 'options', + object: queryOpts.overrides, + schemaKey: queryOpts.schemaKey, + options: this._opts ? this._opts.validation : _opts.validation + }); let queryOptions = { - ...query.defaults, // Module defaults e.g. { period: '1wk', lang: 'en' } - ...query.runtime, // Runtime params e.g. { q: query } - ...query.overrides, // User supplied options that override above + ...queryOpts.defaults, // Module defaults e.g. { period: '1wk', lang: 'en' } + ...queryOpts.runtime, // Runtime params e.g. { q: query } + ...queryOpts.overrides, // User supplied options that override above }; /* @@ -112,13 +121,13 @@ export default async function moduleExec(this: ThisWithFetch, opts: ModuleExecOp * the query. Useful to transform options we allow but not Yahoo, e.g. * allow a "2020-01-01" date but transform this to a UNIX epoch. */ - if (query.transformWith) - queryOptions = query.transformWith(queryOptions); + if (queryOpts.transformWith) + queryOptions = queryOpts.transformWith(queryOptions); // this._fetch is lib/yahooFinanceFetch - let result = await this._fetch(query.url, queryOptions, moduleOpts, query.fetchType); + let result = await this._fetch(queryOpts.url, queryOptions, moduleOpts, queryOpts.fetchType); - if (query.fetchType === 'csv') + if (queryOpts.fetchType === 'csv') result = csv2json(result); /* @@ -133,7 +142,7 @@ export default async function moduleExec(this: ThisWithFetch, opts: ModuleExecOp || moduleOpts.validateResult === true; const validationOpts = { - ...this._opts?.validation, + ...(this._opts ? this._opts.validation : _opts.validation), // Set logErrors=false if validateResult=false logErrors: validateResult ? this._opts?.validation?.logErrors : false, }; @@ -156,7 +165,13 @@ export default async function moduleExec(this: ThisWithFetch, opts: ModuleExecOp * database, etc. Otherwise you'll receive an error. */ try { - validateAndCoerceTypes(result, opts.result.schemaKey, undefined, validationOpts); + validateAndCoerceTypes({ + source: moduleName, + type: 'result', + object: result, + schemaKey: resultOpts.schemaKey, + options: validationOpts + }); } catch (error) { if (validateResult) throw error; diff --git a/src/lib/options.ts b/src/lib/options.ts index 5713eac7..27a2c02d 100644 --- a/src/lib/options.ts +++ b/src/lib/options.ts @@ -1,5 +1,6 @@ export default { validation: { - logErrors: true + logErrors: true, + logOptionsErrors: true, } } diff --git a/src/lib/validateAndCoerceTypes.spec.ts b/src/lib/validateAndCoerceTypes.spec.ts index a971791d..8fb3e2cf 100644 --- a/src/lib/validateAndCoerceTypes.spec.ts +++ b/src/lib/validateAndCoerceTypes.spec.ts @@ -1,10 +1,13 @@ import validateAndCoerceTypes, { ajv } from './validateAndCoerceTypes'; import { InvalidOptionsError, FailedYahooValidationError } from './errors'; -const QUERY_RESULT_SCHEMA_KEY = "#/definitions/QuoteSummaryResult"; - -const defaultOptions = { - logErrors: false +const defParams = { + source: "validateAndCoerceTypes.spec.js", + schemaKey: "#/definitions/QuoteSummaryResult", + options: { + logErrors: true, + logOptionsErrors: true, + } }; const priceResult = { @@ -58,7 +61,7 @@ describe('validateAndCoerceTypes', () => { it('passes regular numbers', () => { const result = Object.assign({}, priceResult); result.price = Object.assign({}, result.price); - validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions); + validateAndCoerceTypes({ ...defParams, type: 'result', object: result }); expect(result.price.priceHint).toBe(2); }); @@ -66,7 +69,7 @@ describe('validateAndCoerceTypes', () => { const result = Object.assign({}, priceResult); result.price = Object.assign({}, result.price); result.price.postMarketChangePercent = { raw: 0.006599537, fmt: "6.5%" } - validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions); + validateAndCoerceTypes({ ...defParams, type: 'result', object: result }); expect(result.price.postMarketChangePercent).toBe(0.006599537); }); @@ -76,7 +79,10 @@ describe('validateAndCoerceTypes', () => { /* @ts-ignore */ result.price.postMarketChangePercent = true; expect( - () => validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions) + () => validateAndCoerceTypes({ + ...defParams, type: 'result', object: result, + options: { ...defParams.options, logErrors: false } + }) ).toThrow(/Failed Yahoo Schema/); }); @@ -86,7 +92,23 @@ describe('validateAndCoerceTypes', () => { /* @ts-ignore */ result.price.postMarketChangePercent = { raw: "a string" }; expect( - () => validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions) + () => validateAndCoerceTypes({ + ...defParams, type: 'result', object: result, + options: { ...defParams.options, logErrors: false } + }) + ).toThrow(/Failed Yahoo Schema/); + }); + + it('fails if string returns a NaN', () => { + const result = Object.assign({}, priceResult); + result.price = Object.assign({}, result.price); + /* @ts-ignore */ + result.price.postMarketChangePercent = "not-a-number"; + expect( + () => validateAndCoerceTypes({ + ...defParams, type: 'result', object: result, + options: { ...defParams.options, logErrors: false } + }) ).toThrow(/Failed Yahoo Schema/); }); @@ -100,7 +122,7 @@ describe('validateAndCoerceTypes', () => { // @ts-ignore result.price.regularMarketTime = { raw: 1612313997 }; - validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions); + validateAndCoerceTypes({ ...defParams, type: 'result', object: result }); // @ts-ignore expect(result.price.regularMarketTime.getTime()) .toBe(1612313997 * 1000); @@ -109,8 +131,7 @@ describe('validateAndCoerceTypes', () => { it('coerces epochs', () => { const result = Object.assign({}, priceResult); result.price = Object.assign({}, result.price); - validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions); - // @ts-ignore + validateAndCoerceTypes({ ...defParams, type: 'result', object: result }); // @ts-ignore expect(result.price.regularMarketTime.getTime()) .toBe(new Date(priceResult.price.regularMarketTime).getTime()); @@ -119,7 +140,7 @@ describe('validateAndCoerceTypes', () => { it('coerces recognizable date string', () => { const result = Object.assign({}, priceResult); result.price = Object.assign({}, result.price); - validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions); + validateAndCoerceTypes({ ...defParams, type: 'result', object: result }); // @ts-ignore expect(result.price.regularMarketTime.getTime()) .toBe(new Date(priceResult.price.regularMarketTime).getTime()); @@ -131,7 +152,10 @@ describe('validateAndCoerceTypes', () => { /* @ts-ignore */ result.price.postMarketTime = "clearly not a date"; expect( - () => validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions) + () => validateAndCoerceTypes({ + ...defParams, type: 'result', object: result, + options: { ...defParams.options, logErrors: false } + }) ).toThrow(/Failed Yahoo Schema/); }); @@ -141,7 +165,7 @@ describe('validateAndCoerceTypes', () => { const date = new Date(); // @ts-ignore result.price.postMarketTime = date; - validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions); + validateAndCoerceTypes({ ...defParams, type: 'result', object: result }); expect(result.price.postMarketTime).toBe(date); }); @@ -152,7 +176,14 @@ describe('validateAndCoerceTypes', () => { it('fails on invalid options usage', () => { const options = { period1: true }; expect( - () => validateAndCoerceTypes(options, "#/definitions/HistoricalOptions", "historical") + () => validateAndCoerceTypes({ + ...defParams, + object: options, + type: 'options', + schemaKey: "#/definitions/HistoricalOptions", + source: "historical-in-validate.spec", + options: { ...defParams.options, logOptionsErrors: false } + }) ).toThrow(InvalidOptionsError) }); @@ -164,7 +195,10 @@ describe('validateAndCoerceTypes', () => { let error: FailedYahooValidationError; try { - validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions); + validateAndCoerceTypes({ + ...defParams, object: result, type: 'result', + options: { ...defParams.options, logErrors: false } + }); } catch (e) { error = e; } @@ -192,7 +226,13 @@ describe('validateAndCoerceTypes', () => { it('fails on invalid schema key', () => { expect( - () => validateAndCoerceTypes({}, "SOME_MISSING_KEY") + () => validateAndCoerceTypes({ + ...defParams, + object: {}, + type: 'result', + schemaKey: "SOME_MISSING_KEY", + options: { ...defParams.options, logErrors: false } + }) ).toThrow(/No such schema/) }); @@ -212,7 +252,12 @@ describe('validateAndCoerceTypes', () => { /* @ts-ignore */ console = fakeConsole; expect( - () => validateAndCoerceTypes({ a: 1 }, QUERY_RESULT_SCHEMA_KEY, undefined, { logErrors: true }) + () => validateAndCoerceTypes({ + ...defParams, + object: { a: 1 }, + type: 'result', + options: { ...defParams.options, logErrors: true } + }) ).toThrow("Failed Yahoo Schema validation"); console = origConsole; @@ -226,7 +271,12 @@ describe('validateAndCoerceTypes', () => { /* @ts-ignore */ console = fakeConsole; expect( - () => validateAndCoerceTypes({ a: 1 }, QUERY_RESULT_SCHEMA_KEY, undefined, { logErrors: false }) + () => validateAndCoerceTypes({ + ...defParams, + object: { a: 1 }, + type: 'result', + options: { ...defParams.options, logErrors: false } + }) ).toThrow("Failed Yahoo Schema validation"); console = origConsole; @@ -240,7 +290,10 @@ describe('validateAndCoerceTypes', () => { let error; try { - validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions); + validateAndCoerceTypes({ + ...defParams, object: result, type: "result", + options: { ...defParams.options, logErrors: false } + }); } catch (e) { error = e; } diff --git a/src/lib/validateAndCoerceTypes.ts b/src/lib/validateAndCoerceTypes.ts index 9c956543..b39f18d7 100644 --- a/src/lib/validateAndCoerceTypes.ts +++ b/src/lib/validateAndCoerceTypes.ts @@ -99,21 +99,30 @@ const logObj = process?.stdout?.isTTY interface ValidationOptions { logErrors: boolean; + logOptionsErrors: boolean; } -function validate(object: object, key: string, module?: string, options?: ValidationOptions): void { - const validator = ajv.getSchema(key); +interface ValidateParams { + source: string; + type: "options" | "result"; + object: object; + schemaKey: string; + options: ValidationOptions; +} + +function validate({ source, type, object, schemaKey, options }: ValidateParams): void { + const validator = ajv.getSchema(schemaKey); if (!validator) - throw new Error("No such schema with key: " + key); + throw new Error("No such schema with key: " + schemaKey); const valid = validator(object); if (valid) return; - if (!module) { + if (type === 'result') { - if (!options || options.logErrors === undefined || options.logErrors === true) { - const title = encodeURIComponent("Failed validation: " + key); - console.log("The following result did not validate with schema: " + key); + if (options.logErrors) { + const title = encodeURIComponent("Failed validation: " + schemaKey); + console.log("The following result did not validate with schema: " + schemaKey); logObj(validator.errors); logObj(object); console.log(` @@ -144,11 +153,11 @@ see https://github.com/gadicc/node-yahoo-finance2/tree/devel/docs/validation.md. } else /* if (type === 'options') */ { - if (options?.logErrors) { - console.error(`[yahooFinance.${module}] Invalid options ("${key}")`); + if (options.logOptionsErrors) { + console.error(`[yahooFinance.${source}] Invalid options ("${schemaKey}")`); logObj({ errors: validator.errors, input: object }); } - throw new InvalidOptionsError(`yahooFinance.${module} called with invalid options.`); + throw new InvalidOptionsError(`yahooFinance.${source} called with invalid options.`); } } diff --git a/src/modules/autoc.spec.ts b/src/modules/autoc.spec.ts index afbbf660..4d81443e 100644 --- a/src/modules/autoc.spec.ts +++ b/src/modules/autoc.spec.ts @@ -14,15 +14,12 @@ const yf = { describe('autoc', () => { + // See also common module tests in moduleExec.spec.js + it('passes validation', async () => { await yf.autoc('AAPL', {}, { devel: "autoc-AAPL.json" }) }); - it('throws InvalidOptions on invalid options', async () => { - const rwo = (options:any) => yf.autoc('symbol', options); - await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) - }); - it('throws on unexpected input', async () => { // intentionally return output from "search" API // i.e. invalid input for "autoc" diff --git a/src/modules/historical.spec.ts b/src/modules/historical.spec.ts index 49d8e561..c0f275c4 100644 --- a/src/modules/historical.spec.ts +++ b/src/modules/historical.spec.ts @@ -9,11 +9,14 @@ const yf = { _env, _fetch, _moduleExec, + _opts: { validation: { logErrors: true }}, historical }; describe('historical', () => { + // See also common module tests in moduleExec.spec.js + it('passes validation', async () => { const result = await yf.historical('AAPL', { period1: "2020-01-01", @@ -36,9 +39,4 @@ describe('historical', () => { }); - it('throws InvalidOptions on invalid options', async () => { - const rwo = (options:any) => yf.historical('symbol', options); - await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) - }); - }); diff --git a/src/modules/quoteSummary.spec.ts b/src/modules/quoteSummary.spec.ts index 042cb94b..e5748c3e 100644 --- a/src/modules/quoteSummary.spec.ts +++ b/src/modules/quoteSummary.spec.ts @@ -48,10 +48,7 @@ describe('quoteSummary', () => { describe('quoteSummary', () => { - it('throws InvalidOptions on invalid options', async () => { - const rwo = (options:any) => yf.quoteSummary('symbol', options); - await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) - }); + // See also common module tests in moduleExec.spec.js it('throws on invalid result', async () => { // intentionally return output from "search" API diff --git a/src/modules/search.spec.ts b/src/modules/search.spec.ts index 64f781e6..f993f16b 100644 --- a/src/modules/search.spec.ts +++ b/src/modules/search.spec.ts @@ -23,6 +23,8 @@ const testSearches = [ describe('search', () => { + // See also common module tests in moduleExec.spec.js + // validate different searches testSearches.forEach((search) => { it(`passed validation for search: ${search}`, async () => { @@ -31,16 +33,4 @@ describe('search', () => { }); }); - it('throws InvalidOptions on invalid options', async () => { - const rwo = (options:any) => yf.search('symbol', options); - await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError) - }); - - it('throws on unexpected input', async () => { - yf._opts.validation.logErrors = false; - await expect(yf.search('AAPL', {}, { devel: 'search-fakeBadResult.json' })) - .rejects.toThrow(/Failed Yahoo Schema/) - yf._opts.validation.logErrors = true; - }); - });