@furystack/rest-service
Version:
Repository implementation for FuryStack
368 lines • 17.7 kB
JavaScript
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