UNPKG

@furystack/rest-service

Version:

Repository implementation for FuryStack

368 lines 17.7 kB
import { getStoreManager, InMemoryStore, User } from '@furystack/core'; import { getPort } from '@furystack/core/port-generator'; import { Injector } from '@furystack/inject'; import { createClient, ResponseError } from '@furystack/rest-client-fetch'; import { usingAsync } from '@furystack/utils'; import { describe, expect, it } from 'vitest'; import { useRestService } from './helpers.js'; import { DefaultSession } from './models/default-session.js'; import { JsonResult } from './request-action-implementation.js'; import schema from './validate.integration.spec.schema.json' with { type: 'json' }; import { Validate } from './validate.js'; // To recreate: yarn ts-json-schema-generator -f tsconfig.json --no-type-check -p packages/rest-service/src/validate.integration.schema.ts -o packages/rest-service/src/validate.integration.spec.schema.json const name = crypto.randomUUID(); const description = crypto.randomUUID(); const version = crypto.randomUUID(); const createValidateApi = async (options = { enableGetSchema: false }) => { const injector = new Injector(); const port = getPort(); getStoreManager(injector).addStore(new InMemoryStore({ model: User, primaryKey: 'username' })); getStoreManager(injector).addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' })); const api = await useRestService({ injector, enableGetSchema: options.enableGetSchema, name, description, version, api: { GET: { '/validate-query': Validate({ schema, schemaName: 'ValidateQuery', })(async ({ getQuery }) => JsonResult({ ...getQuery() })), '/validate-url/:id': Validate({ schema, schemaName: 'ValidateUrl', })(async ({ getUrlParams }) => JsonResult({ ...getUrlParams() })), '/validate-headers': Validate({ schema, schemaName: 'ValidateHeaders', })(async ({ headers }) => JsonResult({ ...headers })), '/mock': undefined, // ToDo: Generator and test '/mock/:id': undefined, // ToDo: Generator and test }, POST: { '/validate-body': Validate({ schema, schemaName: 'ValidateBody', })(async ({ getBody }) => { const body = await getBody(); return JsonResult({ ...body }); }), '/mock': undefined, // ToDo: Generator and test }, PATCH: { '/mock/:id': undefined, // ToDo: Generator and test }, DELETE: { '/mock/:id': undefined, // ToDo: Generator and test }, }, port, root: '/api', }); const client = createClient({ endpointUrl: `http://127.0.0.1:${port}/api`, }); return { [Symbol.asyncDispose]: injector[Symbol.asyncDispose].bind(injector), injector, api, client, }; }; describe('Validation integration tests', () => { describe('swagger.json schema definition', () => { it('Should include name, description and version in the generated swagger.json', async () => { await usingAsync(await createValidateApi({ enableGetSchema: true }), async ({ client }) => { const result = await client({ method: 'GET', action: '/swagger.json', }); expect(result.response.status).toBe(200); expect(result.result).toBeDefined(); // Verify swagger document structure const swaggerJson = result.result; expect(swaggerJson.openapi).toBe('3.1.0'); expect(swaggerJson.info).toBeDefined(); expect(swaggerJson.info?.title).toBe(name); expect(swaggerJson.info?.description).toBe(description); expect(swaggerJson.info?.version).toBe(version); }); }); it('Should return a 404 when not enabled', async () => { await usingAsync(await createValidateApi({ enableGetSchema: false }), async ({ client }) => { try { await client({ method: 'GET', action: '/swagger.json', }); expect.fail('Expected response error but got success'); } catch (error) { expect(error).toBeInstanceOf(ResponseError); expect(error.response.status).toBe(404); } }); }); it('Should return a generated swagger.json when enabled', async () => { await usingAsync(await createValidateApi({ enableGetSchema: true }), async ({ client }) => { const result = await client({ method: 'GET', action: '/swagger.json', }); expect(result.response.status).toBe(200); expect(result.result).toBeDefined(); // Verify swagger document structure const swaggerJson = result.result; expect(swaggerJson.openapi).toBe('3.1.0'); expect(swaggerJson.info).toBeDefined(); expect(swaggerJson.info?.title).toBe(name); expect(swaggerJson.info?.description).toBe(description); expect(swaggerJson.info?.version).toBe(version); expect(swaggerJson.paths).toBeDefined(); // Verify our API endpoints are included expect(swaggerJson.paths?.['/validate-query']).toBeDefined(); expect(swaggerJson.paths?.['/validate-url/{id}']).toBeDefined(); expect(swaggerJson.paths?.['/validate-headers']).toBeDefined(); expect(swaggerJson.paths?.['/validate-body']).toBeDefined(); // Verify components section expect(swaggerJson.components).toBeDefined(); expect(swaggerJson.components?.schemas).toBeDefined(); expect(swaggerJson.components?.schemas?.ValidateQuery).toBeDefined(); expect(swaggerJson.components?.schemas?.ValidateUrl).toBeDefined(); expect(swaggerJson.components?.schemas?.ValidateHeaders).toBeDefined(); expect(swaggerJson.components?.schemas?.ValidateBody).toBeDefined(); }); }); }); describe('Validation metadata', () => { it('Should return 404 when not enabled', async () => { await usingAsync(await createValidateApi({ enableGetSchema: false }), async ({ client }) => { try { await client({ method: 'GET', action: '/schema', headers: { accept: 'application/schema+json', }, }); } catch (error) { expect(error).toBeInstanceOf(ResponseError); expect(error.response.status).toBe(404); } }); }); it('Should return a 406 when the accept header is not supported', async () => { expect.assertions(2); await usingAsync(await createValidateApi({ enableGetSchema: true }), async ({ client }) => { try { await client({ method: 'GET', action: '/schema', headers: { accept: 'text/plain', }, }); } catch (error) { expect(error).toBeInstanceOf(ResponseError); expect(error.response.status).toBe(406); } }); }); it('Should return the validation metadata', async () => { await usingAsync(await createValidateApi({ enableGetSchema: true }), async ({ client }) => { const result = await client({ method: 'GET', action: '/schema', headers: { accept: 'application/schema+json', }, }); expect(result.response.status).toBe(200); expect(result.result).toBeDefined(); expect(result.result.name).toBe(name); expect(result.result.description).toBe(description); expect(result.result.version).toBe(version); expect(result.result.endpoints['/validate-query']).toBeDefined(); expect(result.result.endpoints['/validate-query'].schema).toStrictEqual(schema); expect(result.result.endpoints['/validate-query'].schemaName).toBe('ValidateQuery'); expect(result.result.endpoints['/validate-query'].method).toBe('GET'); expect(result.result.endpoints['/validate-query'].path).toBe('/validate-query'); expect(result.result.endpoints['/validate-query'].isAuthenticated).toBe(false); expect(result.result.endpoints['/validate-url/:id']).toBeDefined(); expect(result.result.endpoints['/validate-url/:id'].schema).toStrictEqual(schema); expect(result.result.endpoints['/validate-url/:id'].schemaName).toBe('ValidateUrl'); expect(result.result.endpoints['/validate-url/:id'].method).toBe('GET'); expect(result.result.endpoints['/validate-url/:id'].path).toBe('/validate-url/:id'); expect(result.result.endpoints['/validate-url/:id'].isAuthenticated).toBe(false); expect(result.result.endpoints['/validate-headers']).toBeDefined(); expect(result.result.endpoints['/validate-headers'].schema).toStrictEqual(schema); expect(result.result.endpoints['/validate-headers'].schemaName).toBe('ValidateHeaders'); expect(result.result.endpoints['/validate-headers'].method).toBe('GET'); expect(result.result.endpoints['/validate-headers'].path).toBe('/validate-headers'); expect(result.result.endpoints['/validate-headers'].isAuthenticated).toBe(false); expect(result.result.endpoints['/validate-body']).toBeDefined(); expect(result.result.endpoints['/validate-body'].schema).toStrictEqual(schema); expect(result.result.endpoints['/validate-body'].schemaName).toBe('ValidateBody'); expect(result.result.endpoints['/validate-body'].method).toBe('POST'); expect(result.result.endpoints['/validate-body'].path).toBe('/validate-body'); expect(result.result.endpoints['/validate-body'].isAuthenticated).toBe(false); expect(result.result.endpoints['/mock']).toBeUndefined(); expect(result.result.endpoints['/mock/:id']).toBeUndefined(); }); }); }); describe('Validation errors', () => { it('Should validate query', async () => { await usingAsync(await createValidateApi(), async ({ client }) => { expect.assertions(5); try { await client({ method: 'GET', action: '/validate-query', query: undefined, }); } catch (error) { if (error instanceof ResponseError) { expect(error.message).toBe('Bad Request'); expect(error.response?.status).toBe(400); const responseJson = await error.response.json(); expect(responseJson.errors[0].params.missingProperty).toEqual('foo'); expect(responseJson.errors[1].params.missingProperty).toEqual('bar'); expect(responseJson.errors[2].params.missingProperty).toEqual('baz'); } } }); }); it('Should validate url', async () => { await usingAsync(await createValidateApi(), async ({ client }) => { expect.assertions(4); try { await client({ method: 'GET', action: '/validate-url/:id', url: undefined, }); } catch (error) { if (error instanceof ResponseError) { expect(error.message).toBe('Bad Request'); expect(error.response?.status).toBe(400); const responseJson = await error.response.json(); expect(responseJson.errors[0].params.type).toEqual('number'); expect(responseJson.errors[0].instancePath).toEqual('/url/id'); } } }); }); it('Should validate headers', async () => { await usingAsync(await createValidateApi(), async ({ client }) => { expect.assertions(3); try { await client({ method: 'GET', action: '/validate-headers', headers: undefined, }); } catch (error) { if (error instanceof ResponseError) { expect(error.message).toBe('Bad Request'); expect(error.response?.status).toBe(400); const responseJson = await error.response.json(); expect(responseJson.errors.find((e) => e.keyword === 'required' && e.message?.includes('foo'))).toBeDefined(); } } }); }); it('Should validate body', async () => { await usingAsync(await createValidateApi(), async ({ client }) => { expect.assertions(3); try { await client({ method: 'POST', action: '/validate-body', body: undefined, }); } catch (error) { if (error instanceof ResponseError) { expect(error.message).toBe('Bad Request'); expect(error.response?.status).toBe(400); const responseJson = await error.response.json(); expect(responseJson.errors[0].params.missingProperty).toEqual('body'); } } }); }); }); describe('Validation Success', () => { it('Should validate query', async () => { await usingAsync(await createValidateApi(), async ({ client }) => { const result = await client({ method: 'GET', action: '/validate-query', query: { foo: 'foo', bar: 2, baz: false, }, }); expect(result.response.status).toBe(200); expect(result.result.foo).toBe('foo'); expect(result.result.bar).toBe(2); expect(result.result.baz).toBe(false); }); }); it('Should validate url', async () => { await usingAsync(await createValidateApi(), async ({ client }) => { const result = await client({ method: 'GET', action: '/validate-url/:id', url: { id: 3 }, }); expect(result.response.status).toBe(200); expect(result.result.id).toBe(3); }); }); it('Should validate headers', async () => { await usingAsync(await createValidateApi(), async ({ client }) => { const result = await client({ method: 'GET', action: '/validate-headers', headers: { foo: 'foo', bar: 42, baz: true, }, }); expect(result.response.status).toBe(200); expect(result.result.foo).toBe('foo'); expect(result.result.bar).toBe(42); expect(result.result.baz).toBe(true); }); }); it('Should validate body', async () => { await usingAsync(await createValidateApi(), async ({ client }) => { const result = await client({ method: 'POST', action: '/validate-body', body: { foo: 'foo', bar: 42, baz: true, }, }); expect(result.response.status).toBe(200); expect(result.result.foo).toBe('foo'); expect(result.result.bar).toBe(42); expect(result.result.baz).toBe(true); }); }); }); }); //# sourceMappingURL=validate.integration.spec.js.map