mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-24 22:48:06 +00:00
237 lines
7.0 KiB
TypeScript
237 lines
7.0 KiB
TypeScript
import type { MockInstance } from 'vitest';
|
|
import { ZodError, z } from 'zod/v4';
|
|
import * as httpMock from '~test/http-mock.ts';
|
|
import { logger, partial } from '~test/util.ts';
|
|
import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages.ts';
|
|
import * as packageCache from '../../util/cache/package/index.ts';
|
|
import { HttpError } from '../../util/http/index.ts';
|
|
import type { HttpOptions } from '../../util/http/types.ts';
|
|
import { Datasource } from './datasource.ts';
|
|
import type { GetReleasesConfig, ReleaseResult } from './types.ts';
|
|
|
|
const exampleUrl = 'https://example.com/';
|
|
|
|
const ExampleResponse = z.object({ version: z.string() });
|
|
|
|
class TestDatasource extends Datasource {
|
|
constructor() {
|
|
super('test');
|
|
}
|
|
|
|
async getReleases(
|
|
_getReleasesConfig: GetReleasesConfig,
|
|
): Promise<ReleaseResult | null> {
|
|
try {
|
|
await this.http.get(exampleUrl);
|
|
} catch (err) {
|
|
this.handleGenericErrors(err);
|
|
}
|
|
return Promise.resolve(null);
|
|
}
|
|
}
|
|
|
|
class CachedDatasource extends Datasource {
|
|
constructor() {
|
|
super('test');
|
|
}
|
|
|
|
getReleases(config: GetReleasesConfig): Promise<ReleaseResult | null> {
|
|
return this.cached({ key: config.packageName }, () =>
|
|
Promise.resolve<ReleaseResult>({ releases: [{ version: '1.0.0' }] }),
|
|
);
|
|
}
|
|
|
|
getConstantValue(): Promise<string> {
|
|
return this.cached(
|
|
{ namespace: '_test-namespace', key: 'constant' },
|
|
() => 'value',
|
|
);
|
|
}
|
|
}
|
|
|
|
class JsonDatasource extends Datasource {
|
|
constructor() {
|
|
super('test');
|
|
}
|
|
|
|
getReleases(
|
|
_getReleasesConfig: GetReleasesConfig,
|
|
): Promise<ReleaseResult | null> {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
fetch(options?: HttpOptions): Promise<z.infer<typeof ExampleResponse>> {
|
|
return this.fetchJson(exampleUrl, ExampleResponse, options);
|
|
}
|
|
|
|
fetchOrNull(
|
|
options?: HttpOptions,
|
|
): Promise<z.infer<typeof ExampleResponse> | null> {
|
|
return this.fetchJsonOrNull(exampleUrl, ExampleResponse, options);
|
|
}
|
|
}
|
|
|
|
describe('modules/datasource/datasource', () => {
|
|
it('defaults to the first registry strategy', () => {
|
|
expect(new TestDatasource().registryStrategy).toBe('first');
|
|
});
|
|
|
|
it('should throw on 429', async () => {
|
|
const testDatasource = new TestDatasource();
|
|
|
|
httpMock.scope(exampleUrl).get('/').reply(429);
|
|
|
|
await expect(
|
|
testDatasource.getReleases(partial<GetReleasesConfig>()),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('should throw on statusCode >=500 && <600', async () => {
|
|
const testDatasource = new TestDatasource();
|
|
|
|
httpMock.scope(exampleUrl).get('/').reply(504);
|
|
|
|
await expect(
|
|
testDatasource.getReleases(partial<GetReleasesConfig>()),
|
|
).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
describe('fetchJson', () => {
|
|
it('returns the validated body', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock.scope(exampleUrl).get('/').reply(200, { version: '1.0.0' });
|
|
|
|
await expect(datasource.fetch()).resolves.toEqual({ version: '1.0.0' });
|
|
});
|
|
|
|
it('passes the options to the http client', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock
|
|
.scope(exampleUrl, { reqheaders: { 'x-some-header': 'some-value' } })
|
|
.get('/')
|
|
.reply(200, { version: '1.0.0' });
|
|
|
|
await expect(
|
|
datasource.fetch({ headers: { 'x-some-header': 'some-value' } }),
|
|
).resolves.toEqual({ version: '1.0.0' });
|
|
});
|
|
|
|
it('throws an external host error for a server error', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock.scope(exampleUrl).get('/').reply(500);
|
|
|
|
await expect(datasource.fetch()).rejects.toThrow(EXTERNAL_HOST_ERROR);
|
|
});
|
|
|
|
it('rethrows other http errors', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock.scope(exampleUrl).get('/').reply(404);
|
|
|
|
await expect(datasource.fetch()).rejects.toThrow(HttpError);
|
|
});
|
|
|
|
it('throws for a response that fails schema validation', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock.scope(exampleUrl).get('/').reply(200, { version: 42 });
|
|
|
|
await expect(datasource.fetch()).rejects.toThrow(ZodError);
|
|
});
|
|
});
|
|
|
|
describe('fetchJsonOrNull', () => {
|
|
it('returns the validated body', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock.scope(exampleUrl).get('/').reply(200, { version: '1.0.0' });
|
|
|
|
await expect(datasource.fetchOrNull()).resolves.toEqual({
|
|
version: '1.0.0',
|
|
});
|
|
});
|
|
|
|
it('passes the options to the http client', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock
|
|
.scope(exampleUrl, { reqheaders: { 'x-some-header': 'some-value' } })
|
|
.get('/')
|
|
.reply(200, { version: '1.0.0' });
|
|
|
|
await expect(
|
|
datasource.fetchOrNull({ headers: { 'x-some-header': 'some-value' } }),
|
|
).resolves.toEqual({ version: '1.0.0' });
|
|
});
|
|
|
|
it('throws an external host error for a server error', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock.scope(exampleUrl).get('/').reply(500);
|
|
|
|
await expect(datasource.fetchOrNull()).rejects.toThrow(
|
|
EXTERNAL_HOST_ERROR,
|
|
);
|
|
});
|
|
|
|
it('rethrows other http errors', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock.scope(exampleUrl).get('/').reply(404);
|
|
|
|
await expect(datasource.fetchOrNull()).rejects.toThrow(HttpError);
|
|
});
|
|
|
|
it('returns null for a response that fails schema validation', async () => {
|
|
const datasource = new JsonDatasource();
|
|
httpMock.scope(exampleUrl).get('/').reply(200, { version: 42 });
|
|
|
|
await expect(datasource.fetchOrNull()).resolves.toBeNull();
|
|
|
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
|
expect.objectContaining({ datasource: 'test', url: exampleUrl }),
|
|
'Ignoring response that failed schema validation',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('cached', () => {
|
|
let setCache: MockInstance<typeof packageCache.setWithRawTtl>;
|
|
|
|
beforeEach(() => {
|
|
setCache = vi.spyOn(packageCache, 'setWithRawTtl').mockResolvedValue();
|
|
});
|
|
|
|
afterEach(() => {
|
|
setCache.mockRestore();
|
|
});
|
|
|
|
it('defaults the namespace to the datasource id', async () => {
|
|
const testDatasource = new CachedDatasource();
|
|
|
|
await expect(
|
|
testDatasource.getReleases(
|
|
partial<GetReleasesConfig>({ packageName: 'foo' }),
|
|
),
|
|
).resolves.toEqual({ releases: [{ version: '1.0.0' }] });
|
|
|
|
expect(setCache).toHaveBeenCalledExactlyOnceWith(
|
|
'datasource-test',
|
|
'cache-decorator:foo',
|
|
{
|
|
cachedAt: expect.any(String),
|
|
value: { releases: [{ version: '1.0.0' }] },
|
|
},
|
|
30,
|
|
);
|
|
});
|
|
|
|
it('supports overriding the namespace', async () => {
|
|
const testDatasource = new CachedDatasource();
|
|
|
|
await expect(testDatasource.getConstantValue()).resolves.toBe('value');
|
|
|
|
expect(setCache).toHaveBeenCalledExactlyOnceWith(
|
|
'_test-namespace',
|
|
'cache-decorator:constant',
|
|
{ cachedAt: expect.any(String), value: 'value' },
|
|
30,
|
|
);
|
|
});
|
|
});
|
|
});
|