@unleash/proxy
Version:
The Unleash Proxy (Open-Source)
895 lines (784 loc) • 23.9 kB
text/typescript
import request, { type Response } from 'supertest';
import { createApp } from '../app';
import metrics from '../examples/metrics.json';
import MockClient from './client.mock';
const unleashUrl = 'http://localhost:4242/test';
const unleashApiToken = 's1';
test('Should return empty list of toggles', () => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
return request(app)
.get('/proxy')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/)
.then((response: Response) => {
expect(response.body.toggles).toEqual([]);
});
});
test('Should return list of toggles', () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
{
name: 'test2',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
return request(app)
.get('/proxy')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body.toggles.length).toEqual(2);
});
});
test('Should handle POST with empty/nonsensical body', async () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
{
name: 'test2',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
await Promise.all(
[{ blah: 'hello' }, undefined, {}].map((body) =>
request(app)
.post('/proxy')
.type('json')
.send(body)
.set('Accept', 'application/json')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body.toggles).toHaveLength(2);
}),
),
);
});
test('Should handle POST with extra context properties', () => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
return request(app)
.post('/proxy')
.send({
context: {
customProperty: 'string',
properties: { otherCustomProperty: 24 },
},
})
.set('Accept', 'application/json')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/);
});
test('Should handle POST with toggle names', () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
{
name: 'test2',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
return request(app)
.post('/proxy')
.send({ toggles: ['test'] })
.set('Accept', 'application/json')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body.toggles).toHaveLength(1);
expect(response.body.toggles[0].name).toBe('test');
});
});
test('Should return list of toggles with custom proxy client key header', () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
{
name: 'test2',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{
proxySecrets,
unleashUrl,
unleashApiToken,
clientKeysHeaderName: 'NotAuthorized',
},
client,
);
client.emit('ready');
return request(app)
.get('/proxy')
.set('NotAuthorized', 'sdf')
.expect(200)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body.toggles.length).toEqual(2);
});
});
test('Should return list of toggles using env with multiple secrets', () => {
process.env.UNLEASH_PROXY_SECRETS = 'secret1,secret2';
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
{
name: 'test2',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const app = createApp({ unleashUrl, unleashApiToken }, client);
client.emit('ready');
return request(app)
.get('/proxy')
.set('Authorization', 'secret2')
.expect(200)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body.toggles.length).toEqual(2);
});
});
test('Should return list of toggles using env with multiple secrets with space', () => {
process.env.UNLEASH_PROXY_SECRETS = 'secret11, secret22';
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
{
name: 'test2',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const app = createApp({ unleashUrl, unleashApiToken }, client);
client.emit('ready');
return request(app)
.get('/proxy')
.set('Authorization', 'secret22')
.expect(200)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body.toggles.length).toEqual(2);
});
});
test('Should send in context to mock', async () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
await request(app)
.get('/proxy?userId=123&tenantId=me')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/);
expect(client.queriedContexts[0].userId).toEqual('123');
expect(client.queriedContexts[0].properties?.tenantId).toEqual('me');
});
test('Should send in context with ip as remoteAddress', async () => {
const userIp = '123.13.13.42';
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken, trustProxy: true },
client,
);
client.emit('ready');
await request(app)
.get('/proxy?userId=123&tenantId=me')
.set('Authorization', 'sdf')
.set('X-Forwarded-For', userIp)
.expect(200)
.expect('Content-Type', /json/);
expect(client.queriedContexts[0].remoteAddress).toEqual(userIp);
});
test('Should remove "undefined" environment field from context', async () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{
unleashUrl,
unleashApiToken,
proxySecrets,
environment: 'test',
},
client,
);
client.emit('ready');
await request(app)
.get('/proxy?userId=123&tenantId=me')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/);
expect(client.queriedContexts[0]).not.toHaveProperty('environment');
});
test('Providing a string for `properties` yields a 400', async () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{
unleashUrl,
unleashApiToken,
proxySecrets,
environment: 'test',
},
client,
);
client.emit('ready');
await request(app)
.get('/proxy?userId=123&properties=string')
.set('Authorization', 'sdf')
.expect(400);
});
test('Should register metrics', () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets },
client,
);
client.emit('ready');
return request(app)
.post('/proxy/client/metrics')
.send(metrics)
.set('Authorization', 'sdf')
.expect(200);
});
test('Should register metrics an /all path as well', () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets },
client,
);
client.emit('ready');
return request(app)
.post('/proxy/all/client/metrics')
.send(metrics)
.set('Authorization', 'sdf')
.expect(200);
});
test('Should require metrics to have correct format', () => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets },
client,
);
client.emit('ready');
return request(app)
.post('/proxy/client/metrics')
.send({ some: 'blob' })
.set('Authorization', 'sdf')
.expect(400);
});
test('Should return errors as JSON', () => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets },
client,
);
client.emit('ready');
return request(app)
.post('/proxy/client/metrics')
.send({ some: 'blob' })
.set('Authorization', 'sdf')
.expect(400)
.expect('Content-Type', /json/);
});
describe.each([
'',
'/all',
'/health',
'/client/features',
'/internal-backstage/prometheus',
])('GET should return not ready', (url) => {
test(`for ${url}`, () => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets },
client,
);
return request(app).get(`/proxy${url}`).expect(503);
});
});
describe.each(['', '/all', '/client/features'])(
'Requires valid token',
(url) => {
test(`Should return 401 if invalid api token for ${url}`, () => {
const client = new MockClient();
const proxySecrets = ['secret'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
return request(app)
.get(`/proxy${url}`)
.set('Authorization', 'I do not know your secret')
.expect(401);
});
test(`Should return 401 if no api token for ${url}`, () => {
const client = new MockClient();
const proxySecrets = ['secret'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
return request(app).get(`/proxy${url}`).expect(401);
});
},
);
describe.each([
{ url: '/health', responseBody: 'ok' },
{
url: '/internal-backstage/prometheus',
responseBody: 'unleash_proxy_up 1',
},
])('Do not require valid token', ({ url, responseBody }) => {
test(`Should not require a token for ${url}`, () => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets },
client,
);
client.emit('ready');
return request(app)
.get(`/proxy${url}`)
.expect(200)
.then((response) => {
expect(response.text).toMatch(responseBody);
});
});
});
test('Should return 504 for proxy', () => {
const client = new MockClient();
const app = createApp({ unleashUrl, unleashApiToken }, client);
return request(app)
.get('/proxy')
.set('Authorization', 'secret2')
.expect(503);
});
test('Should allow every origin (*)', async () => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
const response = await request(app)
.get('/proxy?userId=999&tenantId=me')
.set('Origin', 'https://test.com')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/);
expect(response.headers).toHaveProperty('access-control-allow-origin', '*');
});
test('Should return the same origin based on cors options', async () => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{
proxySecrets,
unleashUrl,
unleashApiToken,
cors: {
origin: [
'https://example.com',
'https://demo.unleash-hosted.com',
],
},
},
client,
);
client.emit('ready');
const response = await request(app)
.get('/proxy?userId=999&tenantId=me')
.set('Origin', 'https://demo.unleash-hosted.com')
.set('Authorization', 'sdf')
.expect(200)
.expect('Content-Type', /json/);
expect(response.headers).toHaveProperty(
'access-control-allow-origin',
'https://demo.unleash-hosted.com',
);
});
test('Should return 400 bad request for malformed JSON', async () => {
const body = '{ toggles": [] }';
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{ proxySecrets, unleashUrl, unleashApiToken },
client,
);
client.emit('ready');
await request(app)
.post('/proxy')
.type('json')
.send(body)
.set('Accept', 'application/json')
.set('Authorization', 'sdf')
.expect(400)
.expect('Content-Type', /json/);
});
test('Should register server SDK', async () => {
const toggles = [
{
name: 'test',
enabled: true,
impressionData: true,
},
];
const client = new MockClient(toggles);
const proxySecrets = ['sdf'];
const app = createApp(
{
unleashUrl,
unleashApiToken,
proxySecrets,
expServerSideSdkConfig: { tokens: ['s1'] },
},
client,
);
client.emit('ready');
const res = await request(app)
.post('/proxy/client/register')
.send({
appName: 'test',
instanceId: 'i1',
sdkVersion: 'custom1',
environment: 'prod',
interval: 10000,
started: new Date(),
strategies: ['default'],
})
.set('Authorization', 'sdf');
expect(res.statusCode).toBe(200);
});
test('Should return all feature toggles', () => {
const client = new MockClient([
{ name: 'a', enabled: true, impressionData: false },
{ name: 'b', enabled: false, impressionData: false },
{ name: 'c', enabled: true, impressionData: true },
]);
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets, enableAllEndpoint: true },
client,
);
client.emit('ready');
return request(app)
.get('/proxy/all')
.set('Authorization', 'sdf')
.expect(200)
.expect((res) => {
expect(res.body.toggles.length).toBe(3);
});
});
test('/client/features should return toggle definitions', () => {
const client = new MockClient([
{ name: 'a', enabled: true, impressionData: false },
{ name: 'b', enabled: false, impressionData: false },
{ name: 'c', enabled: true, impressionData: true },
]);
const proxySecrets = ['sdf'];
const app = createApp(
{
unleashUrl,
unleashApiToken,
proxySecrets,
enableAllEndpoint: true,
expServerSideSdkConfig: { tokens: ['server-side'] },
},
client,
);
client.emit('ready');
return request(app)
.get('/proxy/client/features')
.set('Authorization', 'server-side')
.expect(200)
.expect((res) => {
expect(res.body.features.length).toBe(3);
expect(res.body.features[0].strategies.length).toBe(1);
});
});
test('/client/features should not accept proxy secret', () => {
const client = new MockClient([
{ name: 'a', enabled: true, impressionData: false },
{ name: 'b', enabled: false, impressionData: false },
{ name: 'c', enabled: true, impressionData: true },
]);
const proxySecrets = ['sdf'];
const app = createApp(
{
unleashUrl,
unleashApiToken,
proxySecrets,
enableAllEndpoint: true,
expServerSideSdkConfig: { tokens: ['server-side'] },
},
client,
);
client.emit('ready');
return request(app)
.get('/proxy/client/features')
.set('Authorization', 'sdf')
.expect(401);
});
test('Should return all feature toggles via POST', () => {
const client = new MockClient([
{ name: 'a', enabled: true, impressionData: false },
{ name: 'b', enabled: false, impressionData: false },
{ name: 'c', enabled: true, impressionData: true },
]);
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets, enableAllEndpoint: true },
client,
);
client.emit('ready');
return request(app)
.post('/proxy/all')
.send({
context: {
customProperty: 'string',
properties: { otherCustomProperty: 24 },
},
})
.set('Authorization', 'sdf')
.expect(200)
.expect((res) => {
expect(res.body.toggles.length).toBe(3);
});
});
test('Should return all named feature toggles via POST', () => {
const client = new MockClient([
{ name: 'a', enabled: true, impressionData: false },
{ name: 'b', enabled: false, impressionData: false },
{ name: 'c', enabled: true, impressionData: true },
]);
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets, enableAllEndpoint: true },
client,
);
client.emit('ready');
return request(app)
.post('/proxy/all')
.send({
toggles: ['a'],
context: {
customProperty: 'string',
properties: { otherCustomProperty: 24 },
},
})
.set('Authorization', 'sdf')
.expect(200)
.expect((res) => {
expect(res.body.toggles.length).toBe(1);
expect(res.body.toggles[0].name).toBe('a');
});
});
test('Should return all enabled feature toggles when POST-ing', () => {
const client = new MockClient([
{ name: 'a', enabled: true, impressionData: false },
{ name: 'c', enabled: true, impressionData: true },
]);
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets, enableAllEndpoint: true },
client,
);
client.emit('ready');
return request(app)
.post('/proxy')
.set('Authorization', 'sdf')
.expect(200)
.expect((res) => {
expect(res.body.toggles.length).toBe(2);
});
});
test('Should return 501 when all feature toggles is not enabled', () => {
const client = new MockClient([
{ name: 'a', enabled: true, impressionData: false },
]);
const proxySecrets = ['sdf'];
const app = createApp(
{ unleashUrl, unleashApiToken, proxySecrets },
client,
);
client.emit('ready');
return request(app)
.get('/proxy/all')
.set('Authorization', 'sdf')
.expect(501);
});
describe('Request content-types', () => {
test.each(['/proxy', '/proxy/all'])(
'Should assign default content-type if the request has a body but no content-type (%s)',
async (endpoint) => {
const client = new MockClient();
const payload = {
context: { appName: 'my-app' },
};
const proxySecrets = ['sdf'];
const app = createApp(
{
unleashUrl,
unleashApiToken,
proxySecrets,
enableAllEndpoint: true,
},
client,
);
client.emit('ready');
await request(app)
.post(endpoint)
.set('Authorization', 'sdf')
.set('Content-Type', '')
.send(payload)
.expect(200)
.then(() => {
expect(client.queriedContexts[0].appName).toEqual(
payload.context.appName,
);
});
},
);
test.each(['/proxy', '/proxy/all'])(
'Should reject non-"content-type: application/json" for POST requests to %s',
(endpoint) => {
const client = new MockClient();
const proxySecrets = ['sdf'];
const app = createApp(
{
unleashUrl,
unleashApiToken,
proxySecrets,
enableAllEndpoint: true,
},
client,
);
client.emit('ready');
return request(app)
.post(endpoint)
.set('Authorization', 'sdf')
.set('Content-Type', 'application/html')
.send('<em>reject me!</em>')
.expect(415);
},
);
});