From d9f4cd2b7a09e83fe47737469ad2e669bb13adaa Mon Sep 17 00:00:00 2001 From: Andrew Drake Date: Fri, 19 Jan 2024 19:11:15 -0500 Subject: [PATCH] Add tests to cover error cases. --- tests/basic.ts | 16 +++-- tests/error_handling.ts | 144 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 tests/error_handling.ts diff --git a/tests/basic.ts b/tests/basic.ts index 026efca..3c948ea 100644 --- a/tests/basic.ts +++ b/tests/basic.ts @@ -101,11 +101,17 @@ describe('Template', () => { }); }); - test('#lookup works', () => { - template.parse('{{ define "foo" }}{{ end }}'); - const fooTemplate = template.lookup('foo'); - expect(fooTemplate).toBeInstanceOf(Template); - expect(fooTemplate?.name()).toBe('foo'); + describe('#lookup', () => { + it('works', () => { + template.parse('{{ define "foo" }}{{ end }}'); + const fooTemplate = template.lookup('foo'); + expect(fooTemplate).toBeInstanceOf(Template); + expect(fooTemplate?.name()).toBe('foo'); + }); + + it('returns undefined on invalid templates', () => { + expect(template.lookup('invalid')).toBeUndefined(); + }); }); test('#name works', () => { diff --git a/tests/error_handling.ts b/tests/error_handling.ts new file mode 100644 index 0000000..3420cd0 --- /dev/null +++ b/tests/error_handling.ts @@ -0,0 +1,144 @@ +import * as binding from '..'; +import {Template} from '..'; + +describe('Template', () => { + let template: Template; + + beforeEach(() => { + template = new Template('test_template'); + }); + + test('constructor handles incorrect argument types', () => { + // @ts-expect-error: testing bad arguments + expect(() => new Template(0)).toThrow('A string was expected'); + }); + + test('#delims handles incorrect argument types', () => { + // @ts-expect-error: testing bad arguments + expect(() => template.delims(0, '')).toThrow('A string was expected'); + // @ts-expect-error: testing bad arguments + expect(() => template.delims('', 0)).toThrow('A string was expected'); + }); + + const unaryMethods = [ + 'executeTemplateString', + 'lookup', + 'new', + 'option', + 'parse', + 'parseFiles', + 'parseGlob', + ] as const; + test.each(unaryMethods)('#%s handles incorrect argument types', (name) => { + // @ts-expect-error: testing bad arguments + expect(() => template[name](0)).toThrow('A string was expected'); + }); + + const staticMethods = ['parseFiles', 'parseGlob'] as const; + test.each(staticMethods)('static .%s handles incorrect argument types', (name) => { + // @ts-expect-error: testing bad arguments + expect(() => Template[name](0)).toThrow('A string was expected'); + }); + + describe('#executeString', () => { + it('handles unsupported value types', () => { + expect(() => template.executeString(Symbol())).toThrow('Unsupported value type'); + expect(() => template.executeString([Symbol()])).toThrow('Unsupported value type'); + expect(() => template.executeString({a: Symbol()})).toThrow('Unsupported value type'); + }); + + it('propagates errors from property accesses', () => { + const err = new Error(); + const badObj = {}; + Object.defineProperty(badObj, 'poison', {enumerable: true, get() { throw err; }}); + expect(() => template.executeString(badObj)).toThrow(err); + const badArr: unknown[] = []; + Object.defineProperty(badArr, 0, {enumerable: true, get() { throw err; }}); + expect(() => template.executeString(badArr)).toThrow(err); + }); + }); + + describe('#executeTemplateString', () => { + it('handles unsupported value types', () => { + expect(() => template.executeTemplateString('', Symbol())).toThrow('Unsupported value type'); + }); + + it('handles invalid template names', () => { + expect(() => template.executeTemplateString('invalid')).toThrow('no template "invalid"'); + }); + }); + + describe('#funcs', () => { + it('captures panics', () => { + expect(() => template.funcs({ ['']() {} })).toThrowErrorMatchingInlineSnapshot(`"caught panic: function name "" is not a valid identifier"`); + }); + + it('ignores undefined functions', () => { + // @ts-expect-error: testing invalid args + template.funcs({ myFunc() { return 'hello' }, myUndef: undefined }); + expect(template.parse('{{ myFunc }}').executeString()).toBe('hello'); + expect(() => template.parse('{{ myUndef }}')).toThrow('function "myUndef" not defined'); + }); + + it('handles incorrect argument types', () => { + // @ts-expect-error: testing invalid args + expect(() => template.funcs(null)).toThrow('Cannot convert undefined or null to object'); + }); + + it('handles invalid function types', () => { + // @ts-expect-error: testing invalid args + expect(() => template.funcs({ invalid: 42 })).toThrowErrorMatchingInlineSnapshot(`"Key 'invalid' is not a function"`); + }); + + it('handles un-mappable Go types', () => { + const myFunc = jest.fn(); + template.funcs({myFunc}).parse('{{ myFunc 123i }}'); + expect(() => template.executeString()).toThrow("can't convert Go value of type complex128"); + // TODO: List and map with Sprig functions + expect(myFunc).not.toHaveBeenCalled(); + }); + + it('propagates errors from property accesses', () => { + const err = new Error(); + const funcs = {}; + Object.defineProperty(funcs, 'poison', {enumerable: true, get() { throw err; }}); + expect(() => template.funcs(funcs)).toThrow(err); + }); + }); + + test('#parseFiles propagates errors', () => { + expect(() => template.parseFiles('/invalid/path/to/template/file')).toThrow('no such file or directory'); + }); + + test('#parseGlob propagates errors', () => { + expect(() => template.parseGlob('/invalid/path/to/template/dir/*')).toThrow('pattern matches no files'); + }); + + test('static .parseFiles propagates errors', () => { + expect(() => Template.parseFiles('/invalid/path/to/template/file')).toThrow('no such file or directory'); + }); + + test('static .parseGlob propagates errors', () => { + expect(() => Template.parseGlob('/invalid/path/to/template/dir/*')).toThrow('pattern matches no files'); + }); + + test('methods handle missing arguments', () => { + // @ts-expect-error: testing missing argument + expect(() => template.parse()).toThrow('A string was expected'); + }); + + test('methods handle invalid this value', () => { + // @ts-expect-error: testing missing argument + const unwrapped = new Template(); + expect(() => unwrapped.parse('')).toThrow('missing or invalid type tag'); + }); + + test('static methods handle missing arguments', () => { + // @ts-expect-error: testing missing argument + expect(() => Template.parseGlob()).toThrow('A string was expected'); + }); +}); + +test('helpers handle unsupported value types', () => { + expect(() => binding.htmlEscaper(Symbol())).toThrow('Unsupported value type'); +});