mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
587 lines
20 KiB
JavaScript
587 lines
20 KiB
JavaScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import yaml from 'js-yaml';
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
// Get the directory name in ESM
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Helper to create a temp file with given content, return its path
|
|
// Note: This helper does not automatically clean up created files/directories.
|
|
function writeTempFile(baseName, content) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'headplane-test-'));
|
|
const filePath = path.join(dir, baseName);
|
|
fs.writeFileSync(filePath, content);
|
|
return filePath;
|
|
}
|
|
|
|
// Helper to create a temporary YAML config file with minimal required structure
|
|
function writeTempYamlConfig(customConfig = {}) {
|
|
const defaultConfig = {
|
|
debug: false,
|
|
server: {
|
|
host: '127.0.0.1',
|
|
port: 8080,
|
|
cookie_secret: '12345678901234567890123456789012', // Exactly 32 chars
|
|
cookie_secure: false,
|
|
agent: {
|
|
authkey: 'testagentauthkey',
|
|
ttl: 180000,
|
|
cache_path: '/tmp/agent_cache.json',
|
|
},
|
|
},
|
|
headscale: {
|
|
url: 'http://localhost:8081', // Required!
|
|
config_strict: false,
|
|
// api_key or api_key_path might be provided by specific tests or be absent
|
|
},
|
|
// OIDC is optional at the top level, so it's not in defaultConfig by default.
|
|
// Tests requiring OIDC will add the oidc block via customConfig.
|
|
};
|
|
|
|
// Deep merge customConfig into defaultConfig
|
|
function deepMerge(target, source) {
|
|
let output = { ...target };
|
|
if (isObject(target) && isObject(source)) {
|
|
output = { ...target, ...source };
|
|
|
|
for (const key of Object.keys(source)) {
|
|
if (isObject(source[key]) && key in target && isObject(target[key])) {
|
|
output[key] = deepMerge(target[key], source[key]);
|
|
} else {
|
|
output[key] = source[key];
|
|
}
|
|
}
|
|
|
|
const sections = ['oidc', 'server', 'headscale'];
|
|
for (const sectionName of sections) {
|
|
if (output[sectionName] && source[sectionName]) {
|
|
const sectionSource = source[sectionName];
|
|
const sectionDefault = target[sectionName] || {};
|
|
const sectionOutput = output[sectionName];
|
|
|
|
for (const baseKey of ['client_secret', 'cookie_secret', 'api_key']) {
|
|
const valueKey = baseKey;
|
|
const pathKey = `${baseKey}_path`;
|
|
|
|
const sourceHasValue = Object.hasOwn(sectionSource, valueKey);
|
|
const sourceHasPath = Object.hasOwn(sectionSource, pathKey);
|
|
const defaultHasValue = Object.hasOwn(sectionDefault, valueKey);
|
|
const defaultHasPath = Object.hasOwn(sectionDefault, pathKey);
|
|
|
|
if (sourceHasPath && !sourceHasValue && defaultHasValue) {
|
|
delete sectionOutput[valueKey];
|
|
} else if (sourceHasValue && !sourceHasPath && defaultHasPath) {
|
|
delete sectionOutput[pathKey];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function isObject(item) {
|
|
return item && typeof item === 'object' && !Array.isArray(item);
|
|
}
|
|
|
|
if (typeof customConfig === 'string') {
|
|
return writeTempFile('config.yaml', customConfig);
|
|
}
|
|
const mergedConfig = deepMerge(defaultConfig, customConfig);
|
|
return writeTempFile('config.yaml', yaml.dump(mergedConfig));
|
|
}
|
|
|
|
// Basic YAML object to string converter for the helper (no longer primary method)
|
|
function yamlToString(obj, indent = '') {
|
|
return Object.entries(obj)
|
|
.map(([key, value]) => {
|
|
if (typeof value === 'object' && value !== null) {
|
|
return `${indent}${key}:\n${yamlToString(value, `${indent} `)}`;
|
|
}
|
|
return `${indent}${key}: ${JSON.stringify(value)}`;
|
|
})
|
|
.join('\n');
|
|
}
|
|
|
|
// Store original process.env to restore after tests that modify it
|
|
const originalEnv = { ...process.env };
|
|
|
|
describe('Configuration Loading', () => {
|
|
let loadConfig;
|
|
let ConfigError;
|
|
|
|
beforeAll(async () => {
|
|
const module = await import('../app/server/config/loader.ts');
|
|
loadConfig = module.loadConfig;
|
|
ConfigError = module.ConfigError;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// biome-ignore lint/performance/noDelete: Necessary for test environment cleanup
|
|
delete process.env.HEADPLANE_OIDC__CLIENT_SECRET;
|
|
// biome-ignore lint/performance/noDelete: Necessary for test environment cleanup
|
|
delete process.env.HEADPLANE_OIDC__CLIENT_SECRET_PATH;
|
|
// biome-ignore lint/performance/noDelete: Necessary for test environment cleanup
|
|
delete process.env.HEADPLANE_SERVER__COOKIE_SECRET;
|
|
// biome-ignore lint/performance/noDelete: Necessary for test environment cleanup
|
|
delete process.env.HEADPLANE_SERVER__COOKIE_SECRET_PATH;
|
|
// biome-ignore lint/performance/noDelete: Necessary for test environment cleanup
|
|
delete process.env.HEADPLANE_HEADSCALE__API_KEY;
|
|
// biome-ignore lint/performance/noDelete: Necessary for test environment cleanup
|
|
delete process.env.HEADPLANE_HEADSCALE__API_KEY_PATH;
|
|
// biome-ignore lint/performance/noDelete: Necessary for test environment cleanup
|
|
delete process.env.TEST_SECRET_DIR;
|
|
|
|
// Create a dummy agent_cache.json file as the loader tries to read it
|
|
// when server.agent.cache_path is defined and loadEnv might be true.
|
|
const dummyCachePath = '/tmp/agent_cache.json';
|
|
if (!fs.existsSync(dummyCachePath)) {
|
|
// Ensure directory exists if it's deeper, though /tmp should be fine
|
|
const dirname = path.dirname(dummyCachePath);
|
|
if (!fs.existsSync(dirname)) {
|
|
fs.mkdirSync(dirname, { recursive: true });
|
|
}
|
|
fs.writeFileSync(dummyCachePath, JSON.stringify({}));
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
Object.assign(process.env, originalEnv);
|
|
});
|
|
|
|
describe('OIDC Configuration', () => {
|
|
const minimalOidcRequiredFields = {
|
|
token_endpoint_auth_method: 'client_secret_basic',
|
|
disable_api_key_login: false,
|
|
headscale_api_key: 'dummyHeadscaleApiKeyForOidcBlock',
|
|
// client_secret or client_secret_path is provided by each test as needed
|
|
};
|
|
|
|
it('should load client_secret directly from YAML', async () => {
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client',
|
|
client_secret: 'yaml-direct-oidc-secret',
|
|
...minimalOidcRequiredFields,
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.oidc.client_secret).toBe('yaml-direct-oidc-secret');
|
|
expect(config.oidc.client_secret_path).toBeUndefined();
|
|
});
|
|
|
|
it('should load client_secret from file specified in client_secret_path', async () => {
|
|
const secretValue = 'yaml-file-oidc-secret';
|
|
const secretFilePath = writeTempFile('oidc_secret.txt', secretValue);
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client',
|
|
client_secret_path: secretFilePath,
|
|
...minimalOidcRequiredFields,
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.oidc.client_secret).toBe(secretValue);
|
|
});
|
|
|
|
it('should override YAML client_secret with environment variable', async () => {
|
|
const envSecret = 'env-direct-oidc-secret';
|
|
process.env.HEADPLANE_OIDC__CLIENT_SECRET = envSecret;
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client',
|
|
client_secret: 'yaml-to-be-overridden', // This will be overridden
|
|
...minimalOidcRequiredFields,
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: true, path: tempConfigPath });
|
|
expect(config.oidc.client_secret).toBe(envSecret);
|
|
});
|
|
|
|
it('should override YAML client_secret_path with environment variable', async () => {
|
|
const secretValue = 'env-file-oidc-secret';
|
|
const secretFilePath = writeTempFile('env_oidc_secret.txt', secretValue);
|
|
process.env.HEADPLANE_OIDC__CLIENT_SECRET_PATH = secretFilePath;
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client',
|
|
// Provide a placeholder client_secret to satisfy schema if issuer/client_id are present,
|
|
// as client_secret_path from YAML will be ignored due to env var taking precedence.
|
|
client_secret: 'placeholder-secret-for-schema-check',
|
|
...minimalOidcRequiredFields,
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: true, path: tempConfigPath });
|
|
expect(config.oidc.client_secret).toBe(secretValue);
|
|
});
|
|
|
|
it('should handle environment variable interpolation in client_secret_path', async () => {
|
|
const secretValue = 'interpolated-env-file-oidc-secret';
|
|
const secretDir = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'headplane-secret-dir-'),
|
|
);
|
|
const secretFilePath = path.join(secretDir, 'interpolated_oidc.txt');
|
|
fs.writeFileSync(secretFilePath, secretValue);
|
|
process.env.TEST_SECRET_DIR = secretDir;
|
|
process.env.HEADPLANE_OIDC__CLIENT_SECRET_PATH =
|
|
'${TEST_SECRET_DIR}/interpolated_oidc.txt';
|
|
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client',
|
|
client_secret: 'placeholder-for-schema-if-path-from-env', // Placeholder for schema validation
|
|
...minimalOidcRequiredFields,
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: true, path: tempConfigPath });
|
|
expect(config.oidc.client_secret).toBe(secretValue);
|
|
});
|
|
|
|
it('should reject when both client_secret and client_secret_path are in YAML', async () => {
|
|
const secretFilePath = writeTempFile('conflict_oidc.txt', 'irrelevant');
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client',
|
|
client_secret: 'yaml-direct-secret',
|
|
client_secret_path: secretFilePath,
|
|
...minimalOidcRequiredFields,
|
|
},
|
|
});
|
|
let thrownError = null;
|
|
try {
|
|
await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(thrownError).not.toBeNull();
|
|
if (thrownError) {
|
|
expect(thrownError.name).toBe('ConfigError');
|
|
expect(thrownError.message).toMatch(
|
|
/Only one of "client_secret" or "client_secret_path" may be set/,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should reject when client_secret_path points to non-existent file', async () => {
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client',
|
|
client_secret_path: '/path/to/non_existent_oidc_secret.txt',
|
|
...minimalOidcRequiredFields,
|
|
},
|
|
});
|
|
await expect(
|
|
loadConfig({ loadEnv: false, path: tempConfigPath }),
|
|
).rejects.toThrow(ConfigError);
|
|
await expect(
|
|
loadConfig({ loadEnv: false, path: tempConfigPath }),
|
|
).rejects.toThrow(/File read error|ENOENT/);
|
|
});
|
|
|
|
it('should reject when client_secret_path has unresolvable env var interpolation', async () => {
|
|
process.env.HEADPLANE_OIDC__CLIENT_SECRET_PATH =
|
|
'${MISSING_TEST_SECRET_DIR}/secret.txt';
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client',
|
|
client_secret: 'placeholder-for-schema-if-path-from-env', // Placeholder for schema validation
|
|
...minimalOidcRequiredFields,
|
|
},
|
|
});
|
|
await expect(
|
|
loadConfig({ loadEnv: true, path: tempConfigPath }),
|
|
).rejects.toThrow(ConfigError);
|
|
await expect(
|
|
loadConfig({ loadEnv: true, path: tempConfigPath }),
|
|
).rejects.toThrow(
|
|
'Environment variable "MISSING_TEST_SECRET_DIR" not found',
|
|
);
|
|
});
|
|
|
|
it('should load from client_secret_path when client_secret is null in YAML', async () => {
|
|
const secretValue = 'oidc-secret-from-file-when-null';
|
|
const secretFilePath = writeTempFile(
|
|
'oidc_secret_via_path_null.txt',
|
|
secretValue,
|
|
);
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
oidc: {
|
|
issuer: 'https://example.com/oidc',
|
|
client_id: 'test-client-null-case',
|
|
client_secret: null, // Explicitly null
|
|
client_secret_path: secretFilePath,
|
|
...minimalOidcRequiredFields, // This will provide other required OIDC fields
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.oidc.client_secret).toBe(secretValue);
|
|
// The loader should delete the path key after processing
|
|
expect(config.oidc.client_secret_path).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Server Configuration', () => {
|
|
it('should load cookie_secret directly from YAML', async () => {
|
|
const validCookieSecret = 'abcdefghijklmnopqrstuvwxyz123456'; // 32 chars
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
server: {
|
|
// host and port will come from defaultConfig
|
|
cookie_secret: validCookieSecret,
|
|
// cookie_secure and agent will come from defaultConfig
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.server.cookie_secret).toBe(validCookieSecret);
|
|
});
|
|
|
|
it('should load cookie_secret from file', async () => {
|
|
const secretValue = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; // Exactly 32 'a's
|
|
const secretFilePath = writeTempFile('cookie_secret.txt', secretValue);
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
server: {
|
|
cookie_secret_path: secretFilePath,
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.server.cookie_secret).toBe(secretValue);
|
|
});
|
|
|
|
it('should reject when both cookie_secret and cookie_secret_path are in YAML', async () => {
|
|
const secretFilePath = writeTempFile(
|
|
'conflict_cookie.txt',
|
|
'irrelevant'.padEnd(32, 'X'),
|
|
);
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
server: {
|
|
cookie_secret: '12345678901234567890123456789012',
|
|
cookie_secret_path: secretFilePath,
|
|
},
|
|
});
|
|
let thrownError = null;
|
|
try {
|
|
await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(thrownError).not.toBeNull();
|
|
if (thrownError) {
|
|
expect(thrownError.name).toBe('ConfigError');
|
|
expect(thrownError.message).toMatch(
|
|
/Only one of "cookie_secret" or "cookie_secret_path" may be set/,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should reject when neither cookie_secret nor cookie_secret_path is provided', async () => {
|
|
const yamlWithoutServerCookieSecret = `
|
|
debug: false
|
|
server:
|
|
host: "127.0.0.1"
|
|
port: 8080
|
|
# cookie_secret and cookie_secret_path are intentionally omitted
|
|
cookie_secure: false
|
|
agent:
|
|
authkey: "testagentauthkey"
|
|
ttl: 180000
|
|
cache_path: "/tmp/agent_cache.json"
|
|
headscale:
|
|
url: "http://localhost:8081"
|
|
config_strict: false
|
|
`;
|
|
const tempConfigPath = writeTempYamlConfig(yamlWithoutServerCookieSecret);
|
|
|
|
await expect(
|
|
loadConfig({ loadEnv: false, path: tempConfigPath }),
|
|
).rejects.toThrow(ConfigError);
|
|
await expect(
|
|
loadConfig({ loadEnv: false, path: tempConfigPath }),
|
|
).rejects.toThrow(
|
|
'Either "cookie_secret" or "cookie_secret_path" must be provided for cookie_secret',
|
|
);
|
|
});
|
|
|
|
it('should allow server.agent.authkey to be null if no path is provided', async () => {
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
server: {
|
|
agent: {
|
|
authkey: null, // Explicitly null, no authkey_path
|
|
// Other agent fields will come from defaultConfig via deepMerge
|
|
},
|
|
},
|
|
// Other top-level required fields (headscale, debug) will come from defaultConfig
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.server.agent.authkey).toBeNull();
|
|
});
|
|
|
|
it('should keep path string for server.agent.cache_path and interpolate env vars', async () => {
|
|
process.env.MY_AGENT_CACHE_SUBDIR = 'test-subdir';
|
|
const cachePathValue =
|
|
'/tmp/my-agent-cache-${MY_AGENT_CACHE_SUBDIR}/cache.json';
|
|
const expectedInterpolatedPath =
|
|
'/tmp/my-agent-cache-test-subdir/cache.json';
|
|
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
server: {
|
|
agent: {
|
|
cache_path: cachePathValue,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Ensure the path does NOT exist, so if loader tries to read it, it would fail
|
|
// For safety, only attempt to delete if it's in /tmp/
|
|
if (
|
|
expectedInterpolatedPath.startsWith('/tmp/') &&
|
|
fs.existsSync(expectedInterpolatedPath)
|
|
) {
|
|
fs.unlinkSync(expectedInterpolatedPath);
|
|
}
|
|
if (
|
|
expectedInterpolatedPath.startsWith('/tmp/') &&
|
|
fs.existsSync(path.dirname(expectedInterpolatedPath))
|
|
) {
|
|
// fs.rmdirSync(path.dirname(expectedInterpolatedPath), { recursive: true }); // Be careful with rmdir
|
|
}
|
|
|
|
const config = await loadConfig({ loadEnv: true, path: tempConfigPath });
|
|
|
|
expect(config.server.agent.cache_path).toBe(expectedInterpolatedPath);
|
|
});
|
|
|
|
it('should load server.agent.authkey from authkey_path when authkey is null in YAML', async () => {
|
|
const secretValue = 'agent-authkey-from-file';
|
|
const secretFilePath = writeTempFile('agent_authkey.txt', secretValue);
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
server: {
|
|
agent: {
|
|
authkey: null, // Explicitly null
|
|
authkey_path: secretFilePath,
|
|
// ttl and cache_path will come from defaultConfig
|
|
},
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.server.agent.authkey).toBe(secretValue);
|
|
expect(config.server.agent.authkey_path).toBeUndefined();
|
|
});
|
|
|
|
it('should reject if both server.agent.authkey and authkey_path are provided', async () => {
|
|
const secretFilePath = writeTempFile(
|
|
'agent_authkey_conflict.txt',
|
|
'some-content',
|
|
);
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
server: {
|
|
agent: {
|
|
authkey: 'direct-agent-authkey',
|
|
authkey_path: secretFilePath,
|
|
},
|
|
},
|
|
});
|
|
let thrownError = null;
|
|
try {
|
|
await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(thrownError).not.toBeNull();
|
|
if (thrownError) {
|
|
expect(thrownError.name).toBe('ConfigError');
|
|
expect(thrownError.message).toMatch(
|
|
/Only one of agent "authkey" or "authkey_path" may be set/,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Headscale Configuration', () => {
|
|
it('should load api_key directly from YAML', async () => {
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
headscale: {
|
|
// url and config_strict will come from defaultConfig
|
|
api_key: 'hs-yaml-direct-api-key',
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.headscale.api_key).toBe('hs-yaml-direct-api-key');
|
|
});
|
|
|
|
it('should load api_key from file', async () => {
|
|
const secretValue = 'hs-file-api-key';
|
|
const secretFilePath = writeTempFile('hs_api_key.txt', secretValue);
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
headscale: {
|
|
api_key_path: secretFilePath,
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.headscale.api_key).toBe(secretValue);
|
|
});
|
|
|
|
it('should allow neither api_key nor api_key_path (optional)', async () => {
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
headscale: {
|
|
// api_key and api_key_path are absent, should be fine
|
|
},
|
|
});
|
|
const config = await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
expect(config.headscale.api_key).toBeUndefined();
|
|
expect(config.headscale.api_key_path).toBeUndefined();
|
|
});
|
|
|
|
it('should reject when both api_key and api_key_path are in YAML', async () => {
|
|
const secretFilePath = writeTempFile('conflict_hs_api.txt', 'irrelevant');
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
headscale: {
|
|
api_key: 'hs-direct-key',
|
|
api_key_path: secretFilePath,
|
|
},
|
|
});
|
|
let thrownError = null;
|
|
try {
|
|
await loadConfig({ loadEnv: false, path: tempConfigPath });
|
|
} catch (e) {
|
|
thrownError = e;
|
|
}
|
|
expect(thrownError).not.toBeNull();
|
|
if (thrownError) {
|
|
expect(thrownError.name).toBe('ConfigError');
|
|
expect(thrownError.message).toMatch(
|
|
/Only one of "api_key" or "api_key_path" may be set/,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should keep path string for headscale.config_path and interpolate env vars', async () => {
|
|
process.env.MY_HS_CONFIG_SUBDIR = 'hs-test-subdir';
|
|
const configPathValue =
|
|
'/etc/headscale-${MY_HS_CONFIG_SUBDIR}/config.yaml';
|
|
const expectedInterpolatedPath =
|
|
'/etc/headscale-hs-test-subdir/config.yaml';
|
|
|
|
const tempConfigPath = writeTempYamlConfig({
|
|
headscale: {
|
|
config_path: configPathValue,
|
|
},
|
|
});
|
|
|
|
const config = await loadConfig({ loadEnv: true, path: tempConfigPath });
|
|
|
|
expect(config.headscale.config_path).toBe(expectedInterpolatedPath);
|
|
});
|
|
});
|
|
});
|