@equinor/fusion-framework-vite-plugin-api-service
Version:
Vite plugin for proxying service discovery and mocking
239 lines (196 loc) • 7.36 kB
text/typescript
import { describe, it, expect, afterAll, vi, afterEach } from 'vitest';
import { createServer, type ViteDevServer } from 'vite';
import nock from 'nock';
import {
createProxyHandler,
DEFAULT_VALUES,
plugin as apiServicePlugin,
type ApiRoute,
} from '../src';
describe('API Service Vite Plugin - Middleware Route', () => {
let server: ViteDevServer | undefined;
const startServer = async (plugin) => {
server = await createServer({
plugins: [plugin],
server: {
port: 0, // Dynamic port assignment
host: 'localhost', // Explicitly bind to localhost
},
});
await server.listen();
return {
serverUrl: `http://localhost:${server.config.server.port}`,
};
};
// Clean up after all tests
afterEach(async () => {
await server?.close();
nock.cleanAll();
});
describe('Routing', () => {
it('should return mocked response for <default_api_proxy>/api/mock', async () => {
const mockData = { message: 'Mocked response' };
const { serverUrl } = await startServer(
apiServicePlugin({
routes: [
{
match: '/api/mock',
middleware: (req, res) => {
expect(req.url).toBe('/api/mock');
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(mockData));
},
},
],
}),
);
const response = await fetch(new URL(`${DEFAULT_VALUES.API_PATH}/api/mock`, serverUrl));
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toEqual(mockData);
});
it('should proxy request to external service', async () => {
const mockData = { id: 1, title: 'Mocked proxied response' };
nock('http://localhost:3001').get('/api/posts').reply(200, mockData);
const { serverUrl } = await startServer(
apiServicePlugin({
routes: [
{
match: '/api/posts',
proxy: {
target: 'http://localhost:3001',
},
},
],
}),
);
const response = await fetch(new URL(`${DEFAULT_VALUES.API_PATH}/api/posts`, serverUrl));
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toEqual(mockData);
});
it('should transform proxy response', async () => {
const mockData = { id: 1, title: 'Mocked proxied response' };
nock('http://localhost:3001').get('/api/posts').reply(200, mockData);
const responseTransformerMock = vi.fn((data) => {
return { ...data, processed: true };
});
const { serverUrl } = await startServer(
apiServicePlugin({
routes: [
{
match: '/api/posts',
proxy: {
target: 'http://localhost:3001',
transformResponse: responseTransformerMock,
},
},
],
}),
);
const response = await fetch(new URL(`${DEFAULT_VALUES.API_PATH}/api/posts`, serverUrl));
expect(responseTransformerMock).toHaveBeenCalledTimes(1);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toEqual({ ...mockData, processed: true });
});
});
describe('Proxy handler', () => {
type Service = { name: string; uri: string };
const serviceDiscoveryUrl = 'http://localhost:666';
const serviceProxyRoute = DEFAULT_VALUES.API_PATH;
const setupMockServices = (number: number): Array<Service> => {
const mockServices: Array<Service> = Array.from({ length: number }).map((_, index) => ({
name: `api_${index}`,
uri: `http://localhost:300${index}`,
}));
// set up mock service discovery
nock(serviceDiscoveryUrl).get('/services').reply(200, mockServices);
// set up mock data for each service
for (const service of mockServices) {
nock(service.uri).get('/api/posts').reply(200, { service: service.name });
}
return mockServices;
};
const serviceDiscoveryHandler = (data: Service[]) => {
const routes = [] as ApiRoute[];
const services = [] as Service[];
for (const service of data) {
const url = new URL(service.uri);
const proxyUrl = `${serviceProxyRoute}/${service.name}`;
services.push({ ...service, uri: proxyUrl });
routes.push({
match: (path) => path.startsWith(`/${service.name}`),
proxy: {
target: url.origin,
rewrite: (path) => {
return path.replace(`/${service.name}`, '');
},
},
});
}
return { data: services, routes };
};
it('should generate routes from proxy handler', async () => {
const numberOfMockServices = 3;
// Mock services
setupMockServices(numberOfMockServices);
const { serverUrl } = await startServer(
apiServicePlugin({
proxyHandler: createProxyHandler(serviceDiscoveryUrl, serviceDiscoveryHandler),
}),
);
const services = await fetch(
new URL(`${DEFAULT_VALUES.SERVICES_PATH}/services`, serverUrl),
).then((res) => res.json());
expect(services.length).toBe(numberOfMockServices);
for (const service of services) {
const endpoint = new URL(`${service.uri}/api/posts`, serverUrl);
expect(endpoint).toMatchObject({
pathname: `${serviceProxyRoute}/${service.name}/api/posts`,
});
const response = await fetch(endpoint).then((res) => res.json());
expect(response).toEqual({ service: service.name });
}
});
it('should allow setting proxy route', async () => {
const proxyRoute = '/@custom-services';
setupMockServices(1);
const { serverUrl } = await startServer(
apiServicePlugin({
proxyHandler: createProxyHandler(serviceDiscoveryUrl, serviceDiscoveryHandler, {
route: proxyRoute,
}),
}),
);
const request = fetch(new URL(`${proxyRoute}/services`, serverUrl));
await expect(request).resolves.toMatchObject({ status: 200 });
});
it('should allow adding route configuration', async () => {
const mockProxyRoutes = setupMockServices(1);
const { serverUrl } = await startServer(
apiServicePlugin({
proxyHandler: createProxyHandler(serviceDiscoveryUrl, serviceDiscoveryHandler),
routes: [
{
match: '/api/posts/:id',
middleware: (req, res) => {
res.end(`Custom route ${req.params?.id}`);
},
},
],
}),
);
// prime discovery
await fetch(new URL(`${DEFAULT_VALUES.SERVICES_PATH}/services`, serverUrl));
const request = fetch(
new URL(`${serviceProxyRoute}/${mockProxyRoutes[0].name}/api/posts`, serverUrl),
).then((res) => res.json());
await expect(request).resolves.toMatchObject({ service: mockProxyRoutes[0].name });
const customRoute = fetch(new URL(`${DEFAULT_VALUES.API_PATH}/api/posts/99`, serverUrl)).then(
(res) => res.text(),
);
expect(await customRoute).toBe('Custom route 99');
});
});
});