mirror of
https://github.com/tale/headplane.git
synced 2026-08-24 19:46:29 +00:00
feat: overhaul config loading
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { dump } from 'js-yaml';
|
||||
import { beforeAll, describe, expect, test } from 'vitest';
|
||||
import { ConfigError } from '~/server/config/error';
|
||||
import { loadConfig, loadConfigFile } from '~/server/config/load';
|
||||
import { clearFakeFiles, createFakeFile } from '../setup/overlay-fs';
|
||||
|
||||
const writeYaml = (filePath: string, content: unknown) => {
|
||||
const yamlContent = dump(content);
|
||||
createFakeFile(filePath, yamlContent);
|
||||
};
|
||||
|
||||
describe('Configuration YAML file loading', () => {
|
||||
beforeAll(() => {
|
||||
clearFakeFiles();
|
||||
});
|
||||
|
||||
test('should correctly parse different types from YAML file', async () => {
|
||||
const filePath = '/config/test-config.yaml';
|
||||
writeYaml(filePath, {
|
||||
headscale: {
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
oidc: {
|
||||
client_id: 'my-client-id',
|
||||
},
|
||||
server: {
|
||||
port: 8000,
|
||||
},
|
||||
integration: {
|
||||
agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const config = await loadConfigFile(filePath);
|
||||
expect(config?.headscale?.url).toBe('http://localhost:8080');
|
||||
expect(config?.oidc?.client_id).toBe('my-client-id');
|
||||
expect(config?.server?.port).toBe(8000);
|
||||
expect(config?.integration?.agent?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should not throw errors for inaccessible file', async () => {
|
||||
await expect(
|
||||
loadConfigFile('/non-existent-path/config.yaml'),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test('should correctly get a finalized config from YAML', async () => {
|
||||
const filePath = '/config/minimal-config.yaml';
|
||||
writeYaml(filePath, {
|
||||
headscale: {
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
server: {
|
||||
cookie_secret: 'thirtytwo-character-cookiesecret',
|
||||
},
|
||||
});
|
||||
|
||||
const config = await loadConfig(filePath);
|
||||
expect(config.headscale.url).toBe('http://localhost:8080');
|
||||
expect(config.server.cookie_secret).toBe(
|
||||
'thirtytwo-character-cookiesecret',
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error for missing required fields', async () => {
|
||||
const filePath = '/config/invalid-config.yaml';
|
||||
writeYaml(filePath, {
|
||||
server: {
|
||||
port: 8000,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(loadConfig(filePath)).rejects.toEqual(
|
||||
expect.objectContaining(
|
||||
ConfigError.from('INVALID_REQUIRED_FIELDS', { messages: [] }),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
import { ConfigError } from '~/server/config/error';
|
||||
import { loadConfig, loadConfigEnv } from '~/server/config/load';
|
||||
|
||||
const envVarSnapshot = { ...process.env };
|
||||
describe('Configuration environment variable handling', () => {
|
||||
beforeEach(() => {
|
||||
process.env = { ...envVarSnapshot };
|
||||
});
|
||||
|
||||
test('should correctly parse different types from env vars', async () => {
|
||||
process.env.HEADPLANE_HEADSCALE__URL = 'http://localhost:8080';
|
||||
process.env.HEADPLANE_OIDC__CLIENT_ID = 'my-client-id';
|
||||
process.env.HEADPLANE_SERVER__PORT = '8000';
|
||||
process.env.HEADPLANE_INTEGRATION__AGENT__ENABLED = 'true';
|
||||
|
||||
const config = await loadConfigEnv();
|
||||
expect(config?.headscale?.url).toBe('http://localhost:8080');
|
||||
expect(config?.oidc?.client_id).toBe('my-client-id');
|
||||
expect(config?.server?.port).toBe(8000);
|
||||
expect(config?.integration?.agent?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should not load env vars without the HEADPLANE_ prefix', async () => {
|
||||
process.env.HEADPLANE_HEADSCALE__URL = 'http://localhost:8080';
|
||||
process.env.OTHER_PREFIX_OIDC__CLIENT_ID = 'should-not-be-loaded';
|
||||
|
||||
const config = await loadConfigEnv();
|
||||
expect(config?.headscale?.url).toBe('http://localhost:8080');
|
||||
expect(config?.oidc?.client_id).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should correctly get a finalized config from env vars', async () => {
|
||||
process.env.HEADPLANE_HEADSCALE__URL = 'http://localhost:8080';
|
||||
process.env.HEADPLANE_SERVER__COOKIE_SECRET =
|
||||
'thirtytwo-character-cookiesecret';
|
||||
|
||||
const config = await loadConfig('./non-existent-path.yaml');
|
||||
expect(config.headscale.url).toBe('http://localhost:8080');
|
||||
expect(config.server.cookie_secret).toBe(
|
||||
'thirtytwo-character-cookiesecret',
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error for missing required fields', async () => {
|
||||
process.env.HEADPLANE_SERVER__PORT = '8000';
|
||||
|
||||
await expect(loadConfig('./non-existent-path.yaml')).rejects.toEqual(
|
||||
expect.objectContaining(
|
||||
ConfigError.from('INVALID_REQUIRED_FIELDS', { messages: [] }),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import type { PartialHeadplaneConfigWithPaths } from '~/server/config/config-schema';
|
||||
import { ConfigError } from '~/server/config/error';
|
||||
import { loadConfigKeyPaths } from '~/server/config/load';
|
||||
import { createFakeFile } from '../setup/overlay-fs';
|
||||
|
||||
describe('Configuration secret path handling', () => {
|
||||
test('should correctly substitute server.cookie_secret', async () => {
|
||||
createFakeFile('/secrets/cookie_secret.txt', 'supersecretcookievalue');
|
||||
const config = {
|
||||
server: {
|
||||
cookie_secret_path: '/secrets/cookie_secret.txt',
|
||||
},
|
||||
} as PartialHeadplaneConfigWithPaths;
|
||||
|
||||
await loadConfigKeyPaths(config);
|
||||
expect(config.server?.cookie_secret).toBe('supersecretcookievalue');
|
||||
});
|
||||
|
||||
test('should throw error for missing secret file', async () => {
|
||||
const config = {
|
||||
server: {
|
||||
cookie_secret_path: '/secrets/missing_cookie_secret.txt',
|
||||
},
|
||||
} as PartialHeadplaneConfigWithPaths;
|
||||
|
||||
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
|
||||
ConfigError.from('MISSING_SECRET_FILE', {
|
||||
pathKey: 'server.cookie_secret_path',
|
||||
filePath: '/secrets/missing_cookie_secret.txt',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error for conflicting secret path and field', async () => {
|
||||
const config = {
|
||||
server: {
|
||||
cookie_secret: 'explicitsecretvalue',
|
||||
cookie_secret_path: '/secrets/cookie_secret.txt',
|
||||
},
|
||||
} as PartialHeadplaneConfigWithPaths;
|
||||
|
||||
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
|
||||
ConfigError.from('CONFLICTING_SECRET_PATH_FIELD', {
|
||||
fieldName: 'server.cookie_secret',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should correctly interpolate env vars in secret paths', async () => {
|
||||
process.env.HP_TEST_COOKIE_SECRET_FILE = 'cookie_secret.txt';
|
||||
createFakeFile(
|
||||
`/secrets/${process.env.HP_TEST_COOKIE_SECRET_FILE}`,
|
||||
'envvarsecretvalue',
|
||||
);
|
||||
|
||||
const config = {
|
||||
server: {
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Test supports interpolation
|
||||
cookie_secret_path: '/secrets/${HP_TEST_COOKIE_SECRET_FILE}',
|
||||
},
|
||||
} as PartialHeadplaneConfigWithPaths;
|
||||
|
||||
await loadConfigKeyPaths(config);
|
||||
expect(config.server?.cookie_secret).toBe('envvarsecretvalue');
|
||||
});
|
||||
|
||||
test('should throw error for missing interpolated env var in secret path', async () => {
|
||||
const config = {
|
||||
server: {
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Test supports interpolation
|
||||
cookie_secret_path: '/secrets/${MISSING_ENV_VAR}',
|
||||
},
|
||||
} as PartialHeadplaneConfigWithPaths;
|
||||
|
||||
await expect(loadConfigKeyPaths(config)).rejects.toMatchObject(
|
||||
ConfigError.from('MISSING_INTERPOLATION_VARIABLE', {
|
||||
pathKey: 'server.cookie_secret_path',
|
||||
variableName: 'MISSING_ENV_VAR',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user