mirror of
https://github.com/jdalrymple/gitbeaker.git
synced 2026-01-18 15:55:30 +00:00
397 lines
11 KiB
TypeScript
397 lines
11 KiB
TypeScript
import { RateLimiterMemory } from 'rate-limiter-flexible';
|
|
import {
|
|
RequestOptions,
|
|
ResourceOptions,
|
|
createRateLimiters,
|
|
createRequesterFn,
|
|
defaultOptionsHandler,
|
|
formatQuery,
|
|
getMatchingRateLimiter,
|
|
presetResourceArguments,
|
|
} from '../../src/RequesterUtils';
|
|
|
|
jest.mock('rate-limiter-flexible');
|
|
|
|
const methods = ['get', 'put', 'patch', 'delete', 'post'];
|
|
|
|
describe('defaultOptionsHandler', () => {
|
|
const serviceOptions: ResourceOptions = {
|
|
headers: { test: '5' },
|
|
authHeaders: {
|
|
token: () => Promise.resolve('1234'),
|
|
},
|
|
url: 'testurl',
|
|
rateLimits: {},
|
|
};
|
|
|
|
it('should not use default request options if not passed', async () => {
|
|
const options = await defaultOptionsHandler(serviceOptions);
|
|
|
|
expect(options.method).toBe('GET');
|
|
});
|
|
|
|
it('should stringify body if it isnt of type FormData', async () => {
|
|
const testBody = { test: 6 };
|
|
const { body, headers } = await defaultOptionsHandler(serviceOptions, {
|
|
method: 'POST',
|
|
body: testBody,
|
|
});
|
|
|
|
expect(headers).toContainEntry(['content-type', 'application/json']);
|
|
expect(body).toBe(JSON.stringify(testBody));
|
|
});
|
|
|
|
it('should not stringify body if it of type FormData', async () => {
|
|
const testBody = new FormData();
|
|
|
|
testBody.set('test', 'one');
|
|
|
|
const { body } = await defaultOptionsHandler(serviceOptions, {
|
|
body: testBody,
|
|
method: 'POST',
|
|
});
|
|
|
|
expect(body).toBeInstanceOf(FormData);
|
|
});
|
|
|
|
it('should not assign the sudo property if omitted', async () => {
|
|
const { headers } = await defaultOptionsHandler(serviceOptions, {
|
|
sudo: undefined,
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(headers?.sudo).toBeUndefined();
|
|
});
|
|
|
|
it('should assign the sudo property if passed', async () => {
|
|
const { headers } = await defaultOptionsHandler(serviceOptions, {
|
|
sudo: 'testsudo',
|
|
});
|
|
|
|
expect(headers?.sudo).toBe('testsudo');
|
|
});
|
|
|
|
it('should assign the prefixUrl property if passed', async () => {
|
|
const { prefixUrl } = await defaultOptionsHandler(serviceOptions);
|
|
|
|
expect(prefixUrl).toBe('testurl');
|
|
});
|
|
|
|
it('should not default searchParams', async () => {
|
|
const { searchParams } = await defaultOptionsHandler(serviceOptions, {
|
|
searchParams: undefined,
|
|
});
|
|
|
|
expect(searchParams).toBeUndefined();
|
|
});
|
|
|
|
it('should format searchParams to an stringified object', async () => {
|
|
const { searchParams } = await defaultOptionsHandler(serviceOptions, {
|
|
searchParams: { a: 5 },
|
|
});
|
|
|
|
expect(searchParams).toBe('a=5');
|
|
});
|
|
|
|
it('should format searchParams to an stringified object and decamelize properties', async () => {
|
|
const { searchParams } = await defaultOptionsHandler(serviceOptions, {
|
|
searchParams: { thisSearchTerm: 5 },
|
|
});
|
|
|
|
expect(searchParams).toBe('this_search_term=5');
|
|
});
|
|
|
|
it('should append dynamic authentication token headers', async () => {
|
|
const { headers } = await defaultOptionsHandler(serviceOptions, {
|
|
sudo: undefined,
|
|
method: 'GET',
|
|
});
|
|
|
|
expect(headers?.token).toBe('1234');
|
|
});
|
|
});
|
|
|
|
describe('createInstance', () => {
|
|
const requestHandler = jest.fn();
|
|
const optionsHandler = jest.fn(() => Promise.resolve({} as RequestOptions));
|
|
const serviceOptions: ResourceOptions = {
|
|
headers: { test: '5' },
|
|
authHeaders: {
|
|
token: () => Promise.resolve('1234'),
|
|
},
|
|
url: 'testurl',
|
|
};
|
|
|
|
it('should have a createInstance function', () => {
|
|
expect(createRequesterFn).toBeFunction();
|
|
});
|
|
|
|
it('should return an object with function names equal to those in the methods array when the createInstance function is called', () => {
|
|
const requester = createRequesterFn(optionsHandler, requestHandler)(serviceOptions);
|
|
|
|
expect(requester).toContainAllKeys(methods);
|
|
|
|
methods.forEach((m) => {
|
|
expect(requester[m]).toBeFunction();
|
|
});
|
|
});
|
|
|
|
it('should call the requestHandler with the correct endpoint when passed to any of the method functions', async () => {
|
|
const testEndpoint = 'test endpoint';
|
|
const requester = createRequesterFn(optionsHandler, requestHandler)(serviceOptions);
|
|
|
|
for (const m of methods) {
|
|
await requester[m](testEndpoint, {});
|
|
|
|
expect(optionsHandler).toHaveBeenCalledWith(
|
|
serviceOptions,
|
|
expect.objectContaining({ method: m.toUpperCase() }),
|
|
);
|
|
expect(requestHandler).toHaveBeenCalledWith(testEndpoint, { rateLimiters: {} });
|
|
}
|
|
});
|
|
|
|
it('should respect the closure variables', async () => {
|
|
const serviceOptions1: ResourceOptions = {
|
|
headers: { test: '5' },
|
|
authHeaders: {
|
|
token: () => Promise.resolve('1234'),
|
|
},
|
|
url: 'testurl',
|
|
rateLimits: {},
|
|
};
|
|
const serviceOptions2: ResourceOptions = {
|
|
headers: { test: '5' },
|
|
authHeaders: {
|
|
token: () => Promise.resolve('1234'),
|
|
},
|
|
url: 'testurl2',
|
|
rateLimits: {},
|
|
};
|
|
|
|
const requesterFn = createRequesterFn(optionsHandler, requestHandler);
|
|
const requesterA = requesterFn(serviceOptions1);
|
|
const requesterB = requesterFn(serviceOptions2);
|
|
|
|
await requesterA.get('test');
|
|
|
|
expect(optionsHandler).toHaveBeenCalledWith(
|
|
serviceOptions1,
|
|
expect.objectContaining({ method: 'GET' }),
|
|
);
|
|
|
|
await requesterB.get('test');
|
|
|
|
expect(optionsHandler).toHaveBeenCalledWith(
|
|
serviceOptions2,
|
|
expect.objectContaining({ method: 'GET' }),
|
|
);
|
|
});
|
|
|
|
it('should pass the rate limiters to the requestHandler function', async () => {
|
|
const testEndpoint = 'test endpoint';
|
|
const requester = createRequesterFn(
|
|
optionsHandler,
|
|
requestHandler,
|
|
)({
|
|
...serviceOptions,
|
|
rateLimits: {
|
|
'*': 40,
|
|
'projects/*/test': {
|
|
method: 'GET',
|
|
limit: 10,
|
|
},
|
|
},
|
|
});
|
|
|
|
await requester.get(testEndpoint, {});
|
|
|
|
expect(requestHandler).toHaveBeenCalledWith(testEndpoint, {
|
|
rateLimiters: {
|
|
'*': expect.toBeFunction(),
|
|
'projects/*/test': {
|
|
method: 'GET',
|
|
limit: expect.toBeFunction(),
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createRateLimiters', () => {
|
|
it('should create rate limiter functions when configured, with a default timeout duration of 60', () => {
|
|
const limiters = createRateLimiters({
|
|
'*': 40,
|
|
'projects/*/test': {
|
|
method: 'GET',
|
|
limit: 10,
|
|
},
|
|
});
|
|
|
|
expect(RateLimiterMemory).toHaveBeenCalledWith({ points: 10, duration: 60 });
|
|
expect(RateLimiterMemory).toHaveBeenCalledWith({ points: 40, duration: 60 });
|
|
|
|
expect(limiters).toStrictEqual({
|
|
'*': expect.toBeFunction(),
|
|
'projects/*/test': {
|
|
method: 'GET',
|
|
limit: expect.toBeFunction(),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should respect a custom timeout duration when passed to createRateLimiters', () => {
|
|
const limiters = createRateLimiters(
|
|
{
|
|
'*': 40,
|
|
'projects/*/test': {
|
|
method: 'GET',
|
|
limit: 10,
|
|
},
|
|
},
|
|
30,
|
|
);
|
|
|
|
expect(RateLimiterMemory).toHaveBeenCalledWith({ points: 10, duration: 30 });
|
|
expect(RateLimiterMemory).toHaveBeenCalledWith({ points: 40, duration: 30 });
|
|
|
|
expect(limiters).toStrictEqual({
|
|
'*': expect.toBeFunction(),
|
|
'projects/*/test': {
|
|
method: 'GET',
|
|
limit: expect.toBeFunction(),
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('presetResourceArguments', () => {
|
|
class A {
|
|
x: number;
|
|
|
|
y?: number;
|
|
|
|
constructor({ x = 8, y }: { x?: number; y?: number } = {}) {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
}
|
|
|
|
it('should preset class with extended properties', () => {
|
|
const { A: B } = presetResourceArguments({ A }, { x: 3 });
|
|
const b = new B();
|
|
|
|
expect(b.x).toBe(3);
|
|
});
|
|
|
|
it('should preset class with default properties', () => {
|
|
const { A: B } = presetResourceArguments({ A });
|
|
const b = new B();
|
|
|
|
expect(b.x).toBe(8);
|
|
});
|
|
|
|
it('should overwrite default properties with extended properties', () => {
|
|
const { A: B } = presetResourceArguments({ A }, { x: 3 });
|
|
const b = new B();
|
|
|
|
expect(b.x).toBe(3);
|
|
});
|
|
|
|
it('should overwrite default properties with custom properties', () => {
|
|
const { A: B } = presetResourceArguments({ A });
|
|
const b = new B({ x: 5 });
|
|
|
|
expect(b.x).toBe(5);
|
|
});
|
|
|
|
it('should overwrite default and extended properties with custom properties', () => {
|
|
const { A: B } = presetResourceArguments({ A }, { x: 2 });
|
|
const b = new B({ x: 5 });
|
|
|
|
expect(b.x).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('formatQuery', () => {
|
|
it('should decamelize keys and stringify the object', () => {
|
|
const string = formatQuery({ test: 6 });
|
|
|
|
expect(string).toBe('test=6');
|
|
});
|
|
|
|
it('should decamelize sub keys in not property and stringify the object', () => {
|
|
const string = formatQuery({ test: 6, not: { test: 7 } });
|
|
|
|
expect(string).toBe('test=6¬%5Btest%5D=7');
|
|
});
|
|
});
|
|
|
|
describe('getMatchingRateLimiter', () => {
|
|
it('should default the method to GET if not passed', async () => {
|
|
const rateLimiter = jest.fn();
|
|
const matchingRateLimiter = getMatchingRateLimiter('endpoint', {
|
|
'*': { method: 'GET', limit: rateLimiter },
|
|
});
|
|
|
|
await matchingRateLimiter();
|
|
|
|
expect(rateLimiter).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should uppercase method for matching', async () => {
|
|
const rateLimiter = jest.fn();
|
|
const matchingRateLimiter = getMatchingRateLimiter('endpoint', {
|
|
'*': { method: 'get', limit: rateLimiter },
|
|
});
|
|
|
|
await matchingRateLimiter();
|
|
|
|
expect(rateLimiter).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should default the rateLimiters to an empty object if not passed and return the default rate of 3000 rpm', () => {
|
|
getMatchingRateLimiter('endpoint');
|
|
|
|
expect(RateLimiterMemory).toHaveBeenCalledWith({ points: 3000, duration: 60 });
|
|
});
|
|
|
|
it('should return the most specific rate limit', async () => {
|
|
const rateLimiter = jest.fn();
|
|
const matchingRateLimiter = getMatchingRateLimiter('endpoint/testing', {
|
|
'*': jest.fn(),
|
|
'endpoint/testing*': rateLimiter,
|
|
});
|
|
|
|
await matchingRateLimiter();
|
|
|
|
expect(rateLimiter).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should return a default rate limit of 3000 rpm if nothing matches', () => {
|
|
getMatchingRateLimiter('endpoint', { someurl: jest.fn() });
|
|
|
|
expect(RateLimiterMemory).toHaveBeenCalledWith({ points: 3000, duration: 60 });
|
|
});
|
|
|
|
it('should handle expanded rate limit options with a particular method and limit', async () => {
|
|
const rateLimiter = jest.fn();
|
|
const matchingRateLimiter = getMatchingRateLimiter('endpoint', {
|
|
'*': { method: 'get', limit: rateLimiter },
|
|
});
|
|
|
|
await matchingRateLimiter();
|
|
|
|
expect(rateLimiter).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle simple rate limit options with a particular limit', async () => {
|
|
const rateLimiter = jest.fn();
|
|
const matchingRateLimiter = getMatchingRateLimiter('endpoint/testing', { '**': rateLimiter });
|
|
|
|
await matchingRateLimiter();
|
|
|
|
expect(rateLimiter).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|