/* eslint-disable @typescript-eslint/no-unused-vars */ import { describe, expect, test } from 'vitest' import { Procedures } from './index.js' import { v } from 'suretype' import { Type } from 'typebox' import { ProcedureHookError, ProcedureError, ProcedureValidationError, } from './errors.js' import { ProcedureCodes } from './procedure-codes.js' describe('Procedures', () => { test('Procedures', () => { const result = Procedures({ onCreate: () => { return undefined }, }) expect(result).toHaveProperty('Create') }) test('Procedures generic context & extended config', () => { interface CustomContext { authToken: string } interface ExtendedConfig { customProp: string optionalProp?: number } const { Create } = Procedures() const {info} = Create( 'TestProcedure', { // should not throw type errors customProp: 'customProp', }, async (ctx) => { // should not throw type errors return ctx.authToken }, ) expect(info.customProp).toEqual('customProp') expect(info.optionalProp).toEqual(undefined) }) test('Create Single Procedures', () => { const { procedure: procedure1, info: info1 } = Procedures().Create( 'test1', {}, async () => { return '1' }, ) const { procedure: procedure2, info: info2 } = Procedures().Create( 'test2', {}, async () => { return '2' }, ) expect(procedure1).toBeDefined() expect(info1).toBeDefined() expect(procedure2).toBeDefined() expect(info2).toBeDefined() }) test('Procedures - Create call', () => new Promise((done) => { let mockHttpCall: any const { Create } = Procedures({ onCreate: ({ handler }) => { mockHttpCall = handler }, }) Create( 'Handler', { schema: { args: v.object({ name: v.string() }), data: v.string(), }, }, async (ctx, args) => { expect(args).toEqual({ name: 'name' }) done() return 'name' }, ) mockHttpCall({}, { name: 'name' }) })) test('Procedures - Create call w/ Typebox', () => new Promise((done) => { let mockHttpCall: any const { Create } = Procedures({ onCreate: ({ handler, config, name }) => { mockHttpCall = handler }, }) Create( 'Handler', { schema: { args: Type.Object({ name: Type.Optional(Type.String()) }), data: Type.String(), }, }, async (ctx, args) => { expect(args).toEqual({ name: 'name' }) done() return 'name' }, ) mockHttpCall({}, { name: 'name' }) })) test('Procedures - Create returns a handler to call/test the Procedure registration', async () => { let ProcedureRegisteredCbHandler: any const { Create } = Procedures({ onCreate: (Procedure) => { ProcedureRegisteredCbHandler = Procedure.handler }, }) const { NamedExportHandler, procedure, info } = Create( 'NamedExportHandler', { description: 'Handler description', schema: { args: v.object({ number: v.number() }), }, }, async (ctx, args) => { return args.number }, ) expect(NamedExportHandler).toBeDefined() expect(procedure).toBeDefined() expect(ProcedureRegisteredCbHandler).toEqual(NamedExportHandler) expect(ProcedureRegisteredCbHandler).toEqual(procedure) const result = NamedExportHandler({}, { number: 1 }) expect(result).toBeDefined() expect(result).toBeInstanceOf(Promise) await expect(result).resolves.toEqual(1) expect(info).toBeDefined() expect(info).toBeInstanceOf(Object) expect(info.schema).toHaveProperty('args') expect(info.schema.args).toEqual({ type: 'object', properties: { number: { type: 'number' } }, }) expect(info).toHaveProperty('description') expect(info.description).toEqual('Handler description') }) test('Procedures - Create args validation w/ no args provided', () => new Promise((done) => { let mockHttpCall: any const { Create } = Procedures({ onCreate: ({ handler, config, name }) => { mockHttpCall = (callArgs: any) => { if (config.validation?.args) { const { errors } = config.validation.args(callArgs) if (errors && 'message' in errors[0]) { expect(errors[0].message).toEqual('must be object') done() return } } handler(callArgs, {}) } }, }) Create( 'test', { schema: { args: v.object({}), }, }, async () => { done() }, ) mockHttpCall() })) test('Procedures - Create args validation w/ missing args', async () => new Promise((done) => { let mockHttpCall: any const { Create } = Procedures({ onCreate: async ({ handler, config, name }) => { mockHttpCall = async (callArgs: any) => { if (config.validation?.args) { const { errors } = config.validation.args(callArgs) expect(errors).toBeDefined() expect(errors?.length).toEqual(2) } try { await handler(callArgs, {}) } catch (e: any) { expect(e).instanceof(ProcedureValidationError) expect(e.errors.length).toEqual(2) done() } } }, }) Create( 'test', { schema: { args: v.object({ name: v.string().required(), id: v.number().required(), email: v.string(), }), }, }, async () => { return }, ) mockHttpCall({}) })) test('Procedures - Create call provides ctx to handler', () => new Promise((done) => { let mockHttpCall: any const { Create } = Procedures<{ testCtx: string }>({ onCreate: ({ handler }) => { mockHttpCall = () => handler({ testCtx: 'testCtx' }) }, }) Create('test', {}, async (ctx, args) => { expect(ctx.testCtx).toEqual('testCtx') done() }) mockHttpCall() })) test('Procedures - Create call provides ctx, local ctx & hook context to handler', () => new Promise((done) => { let mockHttpCall: any const { Create } = Procedures<{ testCtx: string }>({ onCreate: ({ handler }) => { mockHttpCall = () => handler({ testCtx: 'testCtx' }) }, }) Create( 'test', { hook: async () => ({ localCtx: 'localCtx' }), }, async (ctx, args) => { expect(ctx.testCtx).toEqual('testCtx') expect(ctx.localCtx).toEqual('localCtx') expect(ctx.error(400, 'error')).toBeInstanceOf(ProcedureError) done() }, ) mockHttpCall() })) test('Procedure hook can throw local ctx error and is caught', async () => { const { Create } = Procedures() const { TestProcedureHookError } = Create( 'TestProcedureHookError', { hook: async (ctx) => { throw ctx.error(400, 'Local context error') }, }, async () => null, ) try { await TestProcedureHookError({}, {}) } catch (e: any) { expect(e).toBeInstanceOf(ProcedureError) expect(e.code).toEqual(400) expect(e.message).toEqual('Local context error') expect(e.procedureName).toEqual('TestProcedureHookError') } }) test('Procedure hook can throw any error and is caught as ProcedureHookError', async () => { const { Create } = Procedures() const { TestProcedureHookError } = Create( 'TestProcedureHookError', { hook: async (ctx) => { throw new Error('Local context error') }, }, async () => null, ) try { await TestProcedureHookError({}, {}) } catch (e: any) { expect(e).toBeInstanceOf(ProcedureHookError) expect(e.code).toEqual(ProcedureCodes.PRECONDITION_FAILED) expect(e.message).toContain('Local context error') expect(e.procedureName).toEqual('TestProcedureHookError') } }) test('Procedure handler can throw local ctx error and is caught', async () => { const { Create } = Procedures() const { TestProcedureHandlerError } = Create( 'TestProcedureHandlerError', {}, async (ctx) => { throw ctx.error(400, 'Local context error') }, ) try { await TestProcedureHandlerError({}, {}) } catch (e: any) { expect(e).toBeInstanceOf(ProcedureError) expect(e.code).toEqual(400) expect(e.message).toEqual('Local context error') expect(e.procedureName).toEqual('TestProcedureHandlerError') } }) test('Procedure handler can call other Procedures and local ctx is not overwritten', async () => { const { Create } = Procedures() const { TestProcedure1 } = Create( 'TestProcedure1', { hook: async (ctx) => { return { test: 'test1' } }, schema: { args: v.any(), }, }, async (ctx, args) => { if (args) return { local: ctx.test } // this will throw an error when no args are provided throw ctx.error(400, 'Local context error', { local: ctx.test }) }, ) const { TestProcedure2 } = Create( 'TestProcedure2', { hook: async (ctx) => { return { test: 'test2' } }, schema: { args: v.any(), }, }, async (ctx, args) => { return await TestProcedure1(ctx, args) }, ) try { await TestProcedure2({}, undefined) } catch (e: any) { expect(e).toBeInstanceOf(ProcedureError) expect(e.code).toEqual(400) expect(e.message).toEqual('Local context error') expect(e.procedureName).toEqual('TestProcedure1') expect(e.meta.local).toEqual('test1') } const result = await TestProcedure2({}, { any: 'any' }) expect(result).toEqual({ local: 'test1' }) }) test('Procedures - getRegisteredProcedures', () => { const { Create, getProcedures } = Procedures({ onCreate: () => { return undefined }, }) Create( 'test-docs', { schema: { args: v.object({ name: v.string().required() }), data: v.string(), }, }, async () => { return 'test-docs' }, ) expect(getProcedures().get('test-docs')).toBeDefined() expect(getProcedures().get('test-docs')?.config?.schema).toEqual({ args: { type: 'object', properties: { name: { type: 'string', }, }, required: ['name'], }, data: { type: 'string', }, }) }) test('Procedures - context() throws', async () => { interface CustomContext { authToken: string } const { Create } = Procedures() function validateAuthToken(token: string) { return token === 'valid-token' } function getUserFromToken(token: string) { return { id: 'user-id' } } const { CheckIsAuthenticated } = Create( 'CheckIsAuthenticated', { hook: async (ctx) => { if (validateAuthToken(ctx.authToken)) { const user = await getUserFromToken(ctx.authToken) return { user } } throw ctx.error(ProcedureCodes.UNAUTHORIZED, 'Invalid auth token') }, schema: { data: v.string(), }, }, async () => { return 'User authentication is valid' }, ) await expect( CheckIsAuthenticated({ authToken: 'valid-token' }, {}), ).resolves.toEqual('User authentication is valid') await expect( CheckIsAuthenticated({ authToken: 'not-valid-token' }, {}), ).rejects.toThrowError(ProcedureError) }) })