Skip to content

Commit

Permalink
test: all except config
Browse files Browse the repository at this point in the history
  • Loading branch information
CptSchnitz committed Jul 8, 2024
1 parent c1fdff2 commit 4f40d19
Show file tree
Hide file tree
Showing 15 changed files with 396 additions and 30 deletions.
4 changes: 0 additions & 4 deletions config/default.json

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"prelint": "npm run format",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "jest --config=./tests/configurations/jest.config.js",
"test": "SUPPRESS_NO_CONFIG_WARNING=true jest --config=./tests/configurations/jest.config.js",
"prebuild": "npm run clean",
"build": "tsc --project tsconfig.build.json",
"start": "npm run build && cd dist && node ./index.js",
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export async function config<T extends { [typeSymbol]: unknown; $id?: string }>(

const dereferencedSchema = await loadSchema(baseSchema);

console.log(options.localConfigPath);

const localConfig = configPkg.util.loadFileConfigs(options.localConfigPath) as { [key: string]: unknown };
debug('local config: %j', localConfig);

Expand Down
6 changes: 3 additions & 3 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const debug = createDebug('env');

const schemaCompositionKeys = ['oneOf', 'anyOf', 'allOf'] as const;

export function parseSchemaEnv(schema: JSONSchema): EnvMap {
function parseSchemaEnv(schema: JSONSchema): EnvMap {
debug('parsing schema for env values');
const fromEnv: EnvMap = {};

Expand All @@ -27,7 +27,7 @@ export function parseSchemaEnv(schema: JSONSchema): EnvMap {
function iterateOverSchemaObject(schema: JSONSchema, path: string): void {
debug('iterating over schema object at path %s', path);
const type = schema.type;
if (type === 'number' || type === 'string' || type === 'boolean' || type === 'integer' || type === 'null') {
if (type === 'number' || type === 'string' || type === 'boolean' || type === 'integer' || type === 'null') {
return handlePrimitive(schema, type, path);
}

Expand Down Expand Up @@ -76,7 +76,7 @@ export function getEnvValues(schema: JSONSchema): object {

switch (details.type) {
case 'boolean':
value = value === 'true';
value = unparsedValue.toLowerCase() === 'true';
break;
case 'integer':
value = parseInt(unparsedValue);
Expand Down
2 changes: 1 addition & 1 deletion src/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async function requestWrapper(url: string, query?: Record<string, unknown>): Pro
const res = await request(url, { query });
if (res.statusCode > StatusCodes.NOT_FOUND) {
debug('Failed to fetch config. Status code: %d', res.statusCode);
throw createConfigError('httpResponseError', 'Failed to fetch config', await createHttpErrorPayload(res));
throw createConfigError('httpResponseError', 'Failed to fetch', await createHttpErrorPayload(res));
}
return res;
} catch (error) {
Expand Down
31 changes: 14 additions & 17 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,24 @@ import { createConfigError } from './errors';

const debug = createDebug('options');

function getEnvOptions(): Partial<Record<keyof BaseOptions, string>> {
const envOptions: Partial<Record<keyof BaseOptions, string>> = {
configName: process.env.CONFIG_NAME,
configServerUrl: process.env.CONFIG_SERVER_URL,
version: process.env.CONFIG_VERSION,
offlineMode: process.env.CONFIG_OFFLINE_MODE,
ignoreServerIsOlderVersionError: process.env.CONFIG_IGNORE_SERVER_IS_OLDER_VERSION_ERROR,
};

// in order to merge correctly the keys should not exist, undefined is not enough
for (const key in envOptions) {
if (envOptions[key as keyof BaseOptions] === undefined) {
delete envOptions[key as keyof BaseOptions];
}
const envOptions: Partial<Record<keyof BaseOptions, string>> = {
configName: process.env.CONFIG_NAME,
configServerUrl: process.env.CONFIG_SERVER_URL,
version: process.env.CONFIG_VERSION,
offlineMode: process.env.CONFIG_OFFLINE_MODE,
ignoreServerIsOlderVersionError: process.env.CONFIG_IGNORE_SERVER_IS_OLDER_VERSION_ERROR,
};

// in order to merge correctly the keys should not exist, undefined is not enough
for (const key in envOptions) {
if (envOptions[key as keyof BaseOptions] === undefined) {
delete envOptions[key as keyof BaseOptions];
}
return envOptions;
}

let baseOptions: BaseOptions | undefined = undefined;

export function initializeOptions(options: Partial<BaseOptions>): BaseOptions {
const envOptions = getEnvOptions();
debug('initializing options with %j and env %j', options, envOptions);
const mergedOptions = deepmerge(options, envOptions);

Expand Down Expand Up @@ -56,4 +53,4 @@ export function getOptions(): BaseOptions {

debug('returning options');
return baseOptions;
}
}
54 changes: 54 additions & 0 deletions tests/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Interceptable, MockAgent, setGlobalDispatcher } from 'undici';

Check failure on line 1 in tests/config.spec.ts

View workflow job for this annotation

GitHub Actions / Prettier

tests/config.spec.ts#L1

There are issues with this file's formatting, please run Prettier to fix the errors
import { commonDbPartialV1 } from '@map-colonies/schemas';
import { StatusCodes } from 'http-status-codes';
import { config } from '../src/config';

const URL = 'http://localhost:8080';
describe('config', () => {
describe('#config', () => {
describe('httpClient', () => {
let client: Interceptable;
beforeEach(() => {
const agent = new MockAgent();
agent.disableNetConnect();

setGlobalDispatcher(agent);
client = agent.get(URL);
});

it('should return the config with all the default values', async () => {
const configData = {
configName: 'name',
schemaId: commonDbPartialV1.$id,
version: 1,
config: {
host: 'avi',
},
createdAt: 0,
};

client.intercept({ path: '/config/name/1?shouldDereference=true', method: 'GET' }).reply(StatusCodes.OK, configData);

const configInstance = await config({
configName: 'name',
version: 1,
schema: commonDbPartialV1,
configServerUrl: URL,
localConfigPath: '../tests/config',
});

const conf = configInstance.getAll();

expect(conf).toEqual({
host: 'avi',
port: 5432,
username: 'postgres',
password: 'postgres',
ssl: {
enabled: false,
}
});
});
});
});
});
5 changes: 5 additions & 0 deletions tests/config/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{

Check failure on line 1 in tests/config/default.json

View workflow job for this annotation

GitHub Actions / Prettier

tests/config/default.json#L1

There are issues with this file's formatting, please run Prettier to fix the errors
"ssl": {
"enabled": false
}
}
1 change: 1 addition & 0 deletions tests/config/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
2 changes: 1 addition & 1 deletion tests/configurations/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
},
coverageReporters: ['text', 'html'],
collectCoverage: true,
collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!*/node_modules/', '!/vendor/**', '!*/common/**', '!**/models/**', '!<rootDir>/src/*'],
collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!*/node_modules/', '!/vendor/**', '!<rootDir>/src/index.ts'],
coverageDirectory: '<rootDir>/coverage',
rootDir: '../../.',
testMatch: ['<rootDir>/tests/**/*.spec.ts'],
Expand Down
107 changes: 107 additions & 0 deletions tests/env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* eslint-disable @typescript-eslint/naming-convention */

Check failure on line 1 in tests/env.spec.ts

View workflow job for this annotation

GitHub Actions / Prettier

tests/env.spec.ts#L1

There are issues with this file's formatting, please run Prettier to fix the errors
import { satisfies } from 'semver';

Check warning on line 2 in tests/env.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint

tests/env.spec.ts#L2

'satisfies' is defined but never used (@typescript-eslint/no-unused-vars)
import { JSONSchema } from '@apidevtools/json-schema-ref-parser';
import { getEnvValues } from '../src/env';
import { EnvType } from '../src/types';

describe('env', () => {
describe('#getEnvValues', () => {
const OLD_ENV = process.env;

beforeEach(() => {
// jest.resetModules() // Most important - it clears the cache
process.env = { ...OLD_ENV }; // Make a copy
});

afterAll(() => {
process.env = OLD_ENV; // Restore old environment
});

it('should return the env values', () => {
process.env.FOO = 'bar';
const schema = {
type: 'object',
properties: {
foo: { type: 'string', 'x-env-value': 'FOO' },
},
required: ['foo'],
} satisfies JSONSchema;

const envValues = getEnvValues(schema);

expect(envValues).toEqual({ foo: 'bar' });
});

it.each([
['string', 'bar', 'bar'],
['boolean', 'true', true],
['integer', '1', 1],
['number', '1.5', 1.5],
['null', 'null', null],
])(`should return the env values for %s`, (type, value, expected) => {
process.env.FOO = value;
const schema = {
type: 'object',
properties: {
foo: { type: type as EnvType, 'x-env-value': 'FOO' },
},
required: ['foo'],
} satisfies JSONSchema;

const envValues = getEnvValues(schema);

expect(envValues).toEqual({ foo: expected });
});

it('should handle nested objects', () => {
process.env.FOO = 'bar';
const schema = {
type: 'object',
properties: {
foo: { type: 'object', properties: { baz: { type: 'string', 'x-env-value': 'FOO' } } },
},
required: ['foo'],
} satisfies JSONSchema;

const envValues = getEnvValues(schema);

expect(envValues).toEqual({ foo: { baz: 'bar' } });
});

it('should not handle array or any type, or any object inside of an array', () => {
process.env.FOO = 'bar';
process.env.BAZ = 'baz';
const schema = {
type: 'object',
properties: {
foo: { type: 'array', 'x-env-value': 'FOO', items: { type: 'string', 'x-env-value': "BAZ" } },
},
required: ['foo'],
} satisfies JSONSchema;

const envValues = getEnvValues(schema);

expect(envValues).toEqual({});
});

it('should handle values in composed objects', () => {
process.env.FOO = 'bar';
const schema = {
type: 'object',
allOf: [
{
type: 'object',
properties: {
foo: { type: 'string', 'x-env-value': 'FOO' },
},
required: ['foo'],
},
],
} satisfies JSONSchema;

const envValues = getEnvValues(schema);

expect(envValues).toEqual({ foo: 'bar' });
});
});
});
91 changes: 91 additions & 0 deletions tests/httpClient.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Interceptable, MockAgent, setGlobalDispatcher } from 'undici';

Check failure on line 1 in tests/httpClient.spec.ts

View workflow job for this annotation

GitHub Actions / Prettier

tests/httpClient.spec.ts#L1

There are issues with this file's formatting, please run Prettier to fix the errors
import { StatusCodes } from 'http-status-codes';
import { getRemoteConfig, getServerCapabilities } from '../src/httpClient';
import { getOptions } from '../src/options';
import { BaseOptions } from '../src/types';

jest.mock('../src/options');

const URL = 'http://localhost:8080';

const mockedGetOptions = getOptions as jest.MockedFunction<typeof getOptions>;

mockedGetOptions.mockReturnValue({
configServerUrl: URL,
} as BaseOptions);

describe('httpClient', () => {
let client: Interceptable;
beforeEach(() => {
const agent = new MockAgent();
agent.disableNetConnect();

setGlobalDispatcher(agent);
client = agent.get(URL);
});

describe('#getServerCapabilities', () => {
it('should return the server capabilities', async () => {
const capabilities = { serverVersion: '1.0.0', schemasPackageVersion: '1.0.0', pubSubEnabled: true };

client.intercept({ path: '/capabilities', method: 'GET' }).reply(StatusCodes.OK, capabilities);

const result = await getServerCapabilities();

expect(result).toEqual(capabilities);
});

it('should throw an error if the request fails', async () => {
client.intercept({ path: '/capabilities', method: 'GET' }).reply(StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error');

await expect(getServerCapabilities()).rejects.toThrow('Failed to fetch');
});

it('should throw an error if the request fails to be sent', async () => {
client.intercept({ path: '/capabilities', method: 'GET' }).replyWithError(new Error('oh noes'));

await expect(getServerCapabilities()).rejects.toThrow('An error occurred while making the request');
});
});

describe('#getRemoteConfig', () => {
it('should return the remote config', async () => {
const config = {
configName: 'name',
schemaId: 'schema',
version: 1,
config: {},
createdAt: 0,
};

client.intercept({ path: '/config/name/1?shouldDereference=true', method: 'GET' }).reply(StatusCodes.OK, config);

const result = await getRemoteConfig('name', 1);
expect(result).toEqual(config);
});

it('should throw an error if the response is bad request', async () => {
client.intercept({ path: '/config/name/1?shouldDereference=true', method: 'GET' }).reply(StatusCodes.BAD_REQUEST, 'Bad request');

await expect(getRemoteConfig('name', 1)).rejects.toThrow('Invalid request to getConfig');
});

it('should throw an error if the response is not found', async () => {
client.intercept({ path: '/config/name/1?shouldDereference=true', method: 'GET' }).reply(StatusCodes.NOT_FOUND, 'Not found');

await expect(getRemoteConfig('name', 1)).rejects.toThrow('Config with given name and version was not found');
});

it('should throw an error if the request fails', async () => {
client.intercept({ path: '/config/name/1?shouldDereference=true', method: 'GET' }).reply(StatusCodes.INTERNAL_SERVER_ERROR, 'Internal Server Error');

await expect(getRemoteConfig('name', 1)).rejects.toThrow('Failed to fetch');
});

it('should throw an error if the request fails to be sent', async () => {
client.intercept({ path: '/config/name/1?shouldDereference=true', method: 'GET' }).replyWithError(new Error('oh noes'));

await expect(getRemoteConfig('name', 1)).rejects.toThrow('An error occurred while making the request');
});
});
});
Loading

0 comments on commit 4f40d19

Please sign in to comment.