From 8cbb1d921c7e7dacac09fbf8b824e554e7208867 Mon Sep 17 00:00:00 2001 From: taichunmin Date: Tue, 11 Jun 2024 00:31:40 +0800 Subject: [PATCH] add test --- package.json | 2 +- src/ChameleonUltra.ts | 7 ++-- src/ResponseDecoder.test.ts | 76 +++++++++++++++++++++++++++++++++++++ src/ResponseDecoder.ts | 2 +- src/helper.test.ts | 56 +++++++++++++-------------- src/helper.ts | 55 +-------------------------- src/plugin/Debug.test.ts | 45 ++++++++++++++++++++++ src/plugin/Debug.ts | 71 +++++++++++++++++++++++++++++++++- typedoc.json | 3 +- 9 files changed, 228 insertions(+), 89 deletions(-) create mode 100644 src/ResponseDecoder.test.ts create mode 100644 src/plugin/Debug.test.ts diff --git a/package.json b/package.json index 424b3b7..ad1a9bb 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "module": "./dist/index.mjs", "name": "chameleon-ultra.js", "type": "commonjs", - "version": "0.3.8", + "version": "0.3.9", "bugs": { "url": "https://github.com/taichunmin/chameleon-ultra.js/issues" }, diff --git a/src/ChameleonUltra.ts b/src/ChameleonUltra.ts index 97eb04a..1f94f50 100644 --- a/src/ChameleonUltra.ts +++ b/src/ChameleonUltra.ts @@ -1,6 +1,6 @@ import _ from 'lodash' import { Buffer } from '@taichunmin/buffer' -import { errToJson, middlewareCompose, sleep, type MiddlewareComposeFn, versionCompare } from './helper' +import { middlewareCompose, sleep, type MiddlewareComposeFn, versionCompare } from './helper' import { EventAsyncGenerator } from './EventAsyncGenerator' import { EventEmitter } from './EventEmitter' import { type ReadableStream, type UnderlyingSink, type WritableStreamDefaultController, WritableStream } from 'node:stream/web' @@ -212,7 +212,6 @@ export class ChameleonUltra { } catch (err) { err.message = `Failed to connect: ${err.message}` this.emitter.emit('error', err) - this.#debug('error', `${err.message}\n${err.stack}`) if (this.isConnected()) await this.disconnect(err) throw err } @@ -227,7 +226,7 @@ export class ChameleonUltra { try { if (this.#isDisconnecting) return this.#isDisconnecting = true // 避免重複執行 - this.#debug('core', '%s %O', err.message, errToJson(err)) + this.emitter.emit('error', err) this.#debug('core', 'disconnecting...') await this.invokeHook('disconnect', { err }, async (ctx, next) => { try { @@ -345,7 +344,7 @@ export class ChameleonUltra { return resp } } catch (err) { - this.#debug('error', `${err.message}\n${err.stack}`) + this.emitter.emit('error', err) throw err } } diff --git a/src/ResponseDecoder.test.ts b/src/ResponseDecoder.test.ts new file mode 100644 index 0000000..49dc476 --- /dev/null +++ b/src/ResponseDecoder.test.ts @@ -0,0 +1,76 @@ +import * as sut from './ResponseDecoder' +import { Buffer } from '@taichunmin/buffer' + +describe('is not buffer', () => { + test.each([ + { name: 'SlotInfo.fromCmd1019', fn: sut.SlotInfo.fromCmd1019 }, + { name: 'SlotFreqIsEnable.fromCmd1023', fn: sut.SlotFreqIsEnable.fromCmd1023 }, + { name: 'BatteryInfo.fromCmd1025', fn: sut.BatteryInfo.fromCmd1025 }, + { name: 'DeviceSettings.fromCmd1034', fn: sut.DeviceSettings.fromCmd1034 }, + { name: 'Hf14aAntiColl.fromCmd2000', fn: sut.Hf14aAntiColl.fromCmd2000 }, + { name: 'Mf1AcquireStaticNestedRes.fromCmd2003', fn: sut.Mf1AcquireStaticNestedRes.fromCmd2003 }, + { name: 'Mf1DarksideRes.fromCmd2004', fn: sut.Mf1DarksideRes.fromCmd2004 }, + { name: 'Mf1NtDistanceRes.fromCmd2005', fn: sut.Mf1NtDistanceRes.fromCmd2005 }, + { name: 'Mf1NestedRes.fromCmd2006', fn: sut.Mf1NestedRes.fromCmd2006 }, + { name: 'Mf1CheckKeysOfSectorsRes.fromCmd2012', fn: sut.Mf1CheckKeysOfSectorsRes.fromCmd2012 }, + { name: 'Mf1DetectionLog.fromBuffer', fn: sut.Mf1DetectionLog.fromBuffer }, + { name: 'Mf1DetectionLog.fromCmd4006', fn: sut.Mf1DetectionLog.fromCmd4006 }, + { name: 'Mf1EmuSettings.fromCmd4009', fn: sut.Mf1EmuSettings.fromCmd4009 }, + ])('$name should throw error', ({ name, fn }) => { + expect.hasAssertions() + try { + fn('not a buffer' as any) + } catch (err) { + expect(err).toBeInstanceOf(TypeError) + expect(err.message).toMatch(/should be a Buffer/) + } + }) + + test.each([ + { name: 'SlotInfo.fromCmd1019', fn: sut.SlotInfo.fromCmd1019 }, + { name: 'SlotFreqIsEnable.fromCmd1023', fn: sut.SlotFreqIsEnable.fromCmd1023 }, + { name: 'BatteryInfo.fromCmd1025', fn: sut.BatteryInfo.fromCmd1025 }, + { name: 'DeviceSettings.fromCmd1034', fn: sut.DeviceSettings.fromCmd1034 }, + { name: 'Mf1DarksideRes.fromCmd2004', fn: sut.Mf1DarksideRes.fromCmd2004 }, + { name: 'Mf1NtDistanceRes.fromCmd2005', fn: sut.Mf1NtDistanceRes.fromCmd2005 }, + { name: 'Mf1CheckKeysOfSectorsRes.fromCmd2012', fn: sut.Mf1CheckKeysOfSectorsRes.fromCmd2012 }, + { name: 'Mf1DetectionLog.fromBuffer', fn: sut.Mf1DetectionLog.fromBuffer }, + ])('$name should throw error', ({ name, fn }) => { + expect.hasAssertions() + try { + fn(new Buffer(0)) + } catch (err) { + expect(err).toBeInstanceOf(TypeError) + expect(err.message).toMatch(/should be a Buffer/) + } + }) +}) + +describe('Hf14aAntiColl', () => { + test('fromBuffer should throw invalid length error', () => { + expect.hasAssertions() + try { + sut.Hf14aAntiColl.fromBuffer(Buffer.from('01', 'hex')) + } catch (err) { + expect(err).toBeInstanceOf(Error) + expect(err.message).toMatch(/invalid length of/) + } + }) + + test('fromBuffer should throw invalid length error', () => { + expect.hasAssertions() + try { + sut.Hf14aAntiColl.fromBuffer(Buffer.from('040102030404000801', 'hex')) + } catch (err) { + expect(err).toBeInstanceOf(Error) + expect(err.message).toMatch(/invalid length of/) + } + }) +}) + +describe('Mf1DarksideRes', () => { + test('fromBuffer should throw invalid length error', () => { + const actual = sut.Mf1DarksideRes.fromCmd2004(Buffer.from('01', 'hex')) + expect(actual).toMatchObject({ status: 1 }) + }) +}) diff --git a/src/ResponseDecoder.ts b/src/ResponseDecoder.ts index 0c7212b..f571885 100644 --- a/src/ResponseDecoder.ts +++ b/src/ResponseDecoder.ts @@ -108,7 +108,7 @@ export class Hf14aAntiColl { const uidLen = buf[0] if (buf.length < uidLen + 4) throw new Error('invalid length of uid') const atsLen = buf[uidLen + 4] - if (buf.length < uidLen + atsLen + 5) throw new Error('invalid invalid length of ats') + if (buf.length < uidLen + atsLen + 5) throw new Error('invalid length of ats') return bufUnpackToClass(buf, `!${uidLen + 1}p2ss${atsLen + 1}p`, Hf14aAntiColl) } diff --git a/src/helper.test.ts b/src/helper.test.ts index 1902fdc..8844057 100644 --- a/src/helper.test.ts +++ b/src/helper.test.ts @@ -1,4 +1,3 @@ -import _ from 'lodash' import * as sut from './helper' test('sleep', async () => { @@ -9,33 +8,6 @@ test('sleep', async () => { expect(end - start).toBeGreaterThanOrEqual(90) }) -test('errToJson', async () => { - const err = _.merge(new Error('test'), { originalError: new Error('test') }) - const actual = sut.errToJson(err) - expect(actual).toMatchObject({ - name: 'Error', - message: 'test', - originalError: { - name: 'Error', - message: 'test', - }, - }) -}) - -test('jsonStringify', async () => { - const circular = { b: 1 } - const obj = { - circular1: circular, - circular2: circular, - map: new Map([['a', 1]]), - number: 1, - set: new Set([1, 2, 3]), - string: 'abc', - } - const actual = sut.jsonStringify(obj) - expect(actual).toBe('{"circular1":{"b":1},"circular2":"[Circular]","map":{"a":1},"number":1,"set":[1,2,3],"string":"abc"}') -}) - describe('middlewareCompose', () => { test('should have correct order', async () => { const actual: number[] = [] @@ -326,6 +298,18 @@ describe('middlewareCompose', () => { expect(actual).toBe(1) }) + + test('should set default ctx', async () => { + const actual: number[] = [] + await sut.middlewareCompose([ + async (ctx, next) => { + actual.push(1) + await next() + }, + ])() + + expect(actual).toEqual([1]) + }) }) describe('versionCompare', () => { @@ -353,4 +337,20 @@ describe('versionCompare', () => { ])('versionCompare(\'$str1\', \'$str2\') = $expected', ({ str1, str2, expected }) => { expect(sut.versionCompare(str1, str2)).toBe(expected) }) + + test('should throw error when version is invalid', () => { + expect.hasAssertions() + try { + sut.versionCompare('test', '1.0.0') + } catch (err) { + expect(err).toBeInstanceOf(Error) + expect(err.message).toMatch(/invalid version/) + } + try { + sut.versionCompare('1.0.0', 'test') + } catch (err) { + expect(err).toBeInstanceOf(Error) + expect(err.message).toMatch(/invalid version/) + } + }) }) diff --git a/src/helper.ts b/src/helper.ts index 38f1d6d..365e5b2 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -9,7 +9,7 @@ enum MiddlewareStatus { ERROR, } -export function middlewareCompose (middlewares: MiddlewareComposeFn[]): (ctx: Record, next?: MiddlewareComposeFn) => Promise { +export function middlewareCompose (middlewares: MiddlewareComposeFn[]): (ctx?: Record, next?: MiddlewareComposeFn) => Promise { // 型態檢查 if (!_.isArray(middlewares)) throw new TypeError('Middleware stack must be an array!') if (_.some(middlewares, fn => !_.isFunction(fn))) throw new TypeError('Middleware must be composed of functions!') @@ -44,60 +44,9 @@ export async function sleep (ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } -const ERROR_KEYS = [ - 'address', - 'args', - 'code', - 'data', - 'dest', - 'errno', - 'info', - 'message', - 'name', - 'path', - 'port', - 'positions', - 'reason', - 'response.data', - 'response.headers', - 'response.status', - 'source', - 'stack', - 'status', - 'statusCode', - 'statusMessage', - 'syscall', -] as const - -export function errToJson (err: T): Partial { - const tmp: any = { - ..._.pick(err, ERROR_KEYS), - ...(_.isNil(err.originalError) ? {} : { originalError: errToJson(err.originalError) }), - } - return tmp -} - -export function jsonStringify (obj: object, space?: number): string { - try { - const preventCircular = new Set() - return JSON.stringify(obj, function (this: any, key: string, value: any) { - if (Buffer.isBuffer(this[key])) return { type: 'Buffer', hex: this[key].toString('hex') } - if (value instanceof Map) return _.fromPairs([...value.entries()]) - if (value instanceof Set) return [...value.values()] - if (_.isObject(value) && !_.isEmpty(value)) { - if (preventCircular.has(value)) return '[Circular]' - preventCircular.add(value) - } - return value - }, space) - } catch (err) { - return `[UnexpectedJSONParseError]: ${err.message as string}` - } -} - export function versionCompare (str1: string, str2: string): number { const tmp = _.map([str1, str2], (str, idx) => { - const matched = _.trim(str).match(/^(?:(\d+)[.]?)?(?:(\d+)[.]?)?(?:(\d+)[.]?)?/) + const matched = _.trim(str).match(/^(?:(\d+)[.]?)(?:(\d+)[.]?)?(?:(\d+)[.]?)?/) if (_.isNil(matched)) throw new Error(`invalid version: str${idx + 1} = ${str}`) return _.map(matched.slice(1), v => _.isNil(v) ? 0 : _.toSafeInteger(v)) }) diff --git a/src/plugin/Debug.test.ts b/src/plugin/Debug.test.ts new file mode 100644 index 0000000..9c779cb --- /dev/null +++ b/src/plugin/Debug.test.ts @@ -0,0 +1,45 @@ +import _ from 'lodash' +import * as sut from './Debug' + +test('errToJson', async () => { + const err = _.merge(new Error('test'), { originalError: new Error('test') }) + const actual = sut.errToJson(err) + expect(actual).toMatchObject({ + name: 'Error', + message: 'test', + originalError: { + name: 'Error', + message: 'test', + }, + }) +}) + +test('jsonStringify', async () => { + const circular = { b: 1 } + const obj = { + _censored: ['censored', 'censored1'], + bigint: 1n, + censored: 'test', + circular1: circular, + circular2: circular, + date: new Date('2000-01-01T00:00:00Z'), + error: new Error('test'), + map: new Map([['a', 1]]), + number: 1, + set: new Set([1, 2, 3]), + string: 'abc', + } + const actual = JSON.parse(sut.jsonStringify(obj)) + expect(actual).toMatchObject({ + bigint: '1', + censored: '[Censored]', + circular1: { b: 1 }, + circular2: '[Circular]', + date: '2000-01-01T00:00:00.000Z', + error: { message: 'test', name: 'Error' }, + map: { a: 1 }, + number: 1, + set: [1, 2, 3], + string: 'abc', + }) +}) diff --git a/src/plugin/Debug.ts b/src/plugin/Debug.ts index 90e256c..922c7ff 100644 --- a/src/plugin/Debug.ts +++ b/src/plugin/Debug.ts @@ -1,5 +1,6 @@ -import createDebugger, { type Debugger } from 'debug' +import _ from 'lodash' import { type ChameleonPlugin, type PluginInstallContext } from '../ChameleonUltra' +import createDebugger, { type Debugger } from 'debug' export default class ChameleonDebug implements ChameleonPlugin { filter?: ChameleonDebugFilter @@ -8,6 +9,11 @@ export default class ChameleonDebug implements ChameleonPlugin { async install (context: PluginInstallContext): Promise { const { ultra } = context + ultra.emitter.on('error', (err: Error) => { + const errJson = errToJson(err) + ultra.emitter.emit('debug', 'error', jsonStringify(errJson)) + console.error(errJson) + }) ultra.emitter.on('debug', (namespace: string, formatter: any, ...args: [] | any[]) => { if (!(this.filter?.(namespace, formatter, ...args) ?? true)) return const debug = this.debugers.get(namespace) ?? createDebugger(`ultra:${namespace}`) @@ -21,3 +27,66 @@ export default class ChameleonDebug implements ChameleonPlugin { ;((globalThis as any ?? {}).ChameleonUltraJS ?? {}).ChameleonDebug = ChameleonDebug // eslint-disable-line @typescript-eslint/prefer-optional-chain type ChameleonDebugFilter = (namespace: string, formatter: any, ...args: [] | any[]) => boolean + +const ERROR_KEYS = [ + 'address', + 'args', + 'code', + 'data', + 'dest', + 'errno', + 'info', + 'message', + 'name', + 'path', + 'port', + 'positions', + 'reason', + 'response.data', + 'response.headers', + 'response.status', + 'source', + 'stack', + 'status', + 'statusCode', + 'statusMessage', + 'syscall', +] as const + +export function errToJson (err: T): Partial { + const tmp: any = { + ..._.pick(err, ERROR_KEYS), + ...(_.isNil(err.originalError) ? {} : { originalError: errToJson(err.originalError) }), + } + return tmp +} + +export function stringifyClone (obj: any): any { + const preventCircular = new Set() + return _.cloneDeepWith(obj, val1 => { + if (_.isObject(val1) && !_.isEmpty(val1)) { + if (preventCircular.has(val1)) return '[Circular]' + preventCircular.add(val1) + } + if (typeof val1 === 'bigint') return val1.toString() + if (val1 instanceof Error) return errToJson(val1) + if (val1 instanceof Map) return _.fromPairs([...val1.entries()]) + if (val1 instanceof Set) return [...val1.values()] + if (val1 instanceof Date) return val1.toISOString() + }) +} + +export function stringifyReplacer (this: any, key: any, val: any): any { + if (key.length > 1 && key[0] === '_') return undefined + const censored = this?._censored ?? [] + for (const key1 of censored) { + if (!_.hasIn(this, key1)) continue + _.set(this, key1, '[Censored]') + } + delete this?._censored + return this[key] +} + +export function jsonStringify (obj: object, space?: number): string { + return JSON.stringify(stringifyClone(obj), stringifyReplacer, space) +} diff --git a/typedoc.json b/typedoc.json index b5a06e4..0929ba3 100644 --- a/typedoc.json +++ b/typedoc.json @@ -8,8 +8,9 @@ "plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-rename-defaults"], "navigationLinks": { "Demos": "https://github.com/taichunmin/chameleon-ultra.js/blob/master/pages/demos.md", + "GitHub": "https://github.com/taichunmin/chameleon-ultra.js", "NPM": "https://www.npmjs.com/package/chameleon-ultra.js", - "GitHub": "https://github.com/taichunmin/chameleon-ultra.js" + "jsDelivr": "https://www.jsdelivr.com/package/npm/chameleon-ultra.js" }, "entryPoints": [ "src/index.ts",