buster/apps/cli/src/commands/deploy/config/config-loader.test.ts

292 lines
9.5 KiB
TypeScript
Raw Normal View History

import { mkdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import yaml from 'js-yaml';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { BusterConfig } from '../schemas';
import { loadBusterConfig, resolveConfiguration } from './config-loader';
describe('config-loader', () => {
let testDir: string;
let testId: string;
beforeEach(async () => {
testId = Math.random().toString(36).substring(7);
testDir = join(tmpdir(), `buster-cli-test-${testId}`);
await mkdir(testDir, { recursive: true });
});
afterEach(async () => {
await rm(testDir, { recursive: true, force: true });
vi.clearAllMocks();
});
describe('loadBusterConfig', () => {
describe('when no buster.yml file exists', () => {
it('should throw error when no config files are found', async () => {
await expect(loadBusterConfig(testDir)).rejects.toThrow(
2025-09-04 21:22:35 +08:00
`No buster.yml found in ${testDir} or any of its subdirectories`
);
});
it('should throw error for non-existent path', async () => {
const nonExistentPath = join(testDir, 'does-not-exist');
await expect(loadBusterConfig(nonExistentPath)).rejects.toThrow(
`Path does not exist: ${nonExistentPath}`
);
});
2025-09-04 21:22:35 +08:00
it('should return null when given a non-buster file', async () => {
const filePath = join(testDir, 'file.txt');
await writeFile(filePath, 'content');
2025-09-04 21:22:35 +08:00
// When given a file that's not buster.yml, it should return null
await expect(loadBusterConfig(filePath)).rejects.toThrow(
2025-09-04 21:22:35 +08:00
`No buster.yml found in ${filePath} or any of its subdirectories`
);
});
});
describe('when single buster.yml exists', () => {
it('should load a valid buster.yml file', async () => {
const config: BusterConfig = {
projects: [
{
name: 'test-project',
data_source: 'postgres',
database: 'test_db',
schema: 'public',
},
],
};
await writeFile(join(testDir, 'buster.yml'), yaml.dump(config));
2025-09-04 21:22:35 +08:00
const { config: result, configPath } = await loadBusterConfig(testDir);
expect(result.projects).toHaveLength(1);
expect(result.projects[0].name).toBe('test-project');
expect(result.projects[0].data_source).toBe('postgres');
2025-09-04 21:22:35 +08:00
expect(configPath).toBe(join(testDir, 'buster.yml'));
});
it('should handle buster.yaml (with .yaml extension)', async () => {
const config: BusterConfig = {
projects: [
{
name: 'yaml-project',
data_source: 'mysql',
database: 'yaml_db',
schema: 'default',
},
],
};
await writeFile(join(testDir, 'buster.yaml'), yaml.dump(config));
2025-09-04 21:22:35 +08:00
const { config: result, configPath } = await loadBusterConfig(testDir);
expect(result.projects).toHaveLength(1);
expect(result.projects[0].name).toBe('yaml-project');
2025-09-04 21:22:35 +08:00
expect(configPath).toBe(join(testDir, 'buster.yaml'));
});
it('should skip invalid yaml files with warning', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
await writeFile(join(testDir, 'buster.yml'), 'invalid: yaml: content:');
2025-09-04 21:22:35 +08:00
await expect(loadBusterConfig(testDir)).rejects.toThrow('Failed to parse buster.yml');
expect(consoleSpy).toHaveBeenCalled();
});
it('should skip files with invalid schema', async () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const invalidConfig = {
projects: [
{
// Missing required fields
name: 'invalid-project',
},
],
};
await writeFile(join(testDir, 'buster.yml'), yaml.dump(invalidConfig));
2025-09-04 21:22:35 +08:00
await expect(loadBusterConfig(testDir)).rejects.toThrow('Failed to parse buster.yml');
expect(consoleSpy).toHaveBeenCalled();
});
});
2025-09-04 21:22:35 +08:00
describe('when searching for buster.yml in subdirectories', () => {
it('should find buster.yml in subdirectory when searching from parent', async () => {
// Create subdirectory
const subDir = join(testDir, 'subdirectory');
await mkdir(subDir, { recursive: true });
const config: BusterConfig = {
projects: [
{
2025-09-04 21:22:35 +08:00
name: 'sub-project',
data_source: 'postgres',
2025-09-04 21:22:35 +08:00
database: 'db1',
schema: 'public',
},
],
};
2025-09-04 21:22:35 +08:00
// Place buster.yml in the subdirectory
await writeFile(join(subDir, 'buster.yml'), yaml.dump(config));
2025-09-04 21:22:35 +08:00
// Search from parent should find subdirectory's buster.yml
const { config: result, configPath } = await loadBusterConfig(testDir);
expect(result.projects).toHaveLength(1);
2025-09-04 21:22:35 +08:00
expect(result.projects[0].name).toBe('sub-project');
expect(configPath).toBe(join(subDir, 'buster.yml'));
});
2025-09-04 21:22:35 +08:00
it('should find first buster.yml when multiple exist in different subdirectories', async () => {
const subDir1 = join(testDir, 'a');
const subDir2 = join(testDir, 'b');
await mkdir(subDir1, { recursive: true });
await mkdir(subDir2, { recursive: true });
const config1: BusterConfig = {
projects: [
{
name: 'project-a',
2025-09-04 21:22:35 +08:00
data_source: 'bigquery',
database: 'db_a',
2025-09-04 21:22:35 +08:00
schema: 'dataset',
},
],
};
const config2: BusterConfig = {
projects: [
{
2025-09-04 21:22:35 +08:00
name: 'project-b',
data_source: 'postgres',
database: 'db_b',
2025-09-04 21:22:35 +08:00
schema: 'public',
},
],
};
2025-09-04 21:22:35 +08:00
// Place configs in different subdirectories
await writeFile(join(subDir1, 'buster.yml'), yaml.dump(config1));
await writeFile(join(subDir2, 'buster.yml'), yaml.dump(config2));
2025-09-04 21:22:35 +08:00
// Should find the first one (deterministic based on directory order)
const { config: result } = await loadBusterConfig(testDir);
expect(result.projects).toHaveLength(1);
// The actual project found depends on directory traversal order
expect(['project-a', 'project-b']).toContain(result.projects[0].name);
});
});
describe('when handling edge cases', () => {
it('should handle empty projects array', async () => {
const config: BusterConfig = {
projects: [],
};
await writeFile(join(testDir, 'buster.yml'), yaml.dump(config));
await expect(loadBusterConfig(testDir)).rejects.toThrow(
2025-09-04 21:22:35 +08:00
'No projects defined in buster.yml'
);
});
it('should provide informative console output', async () => {
const consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
2025-09-04 21:22:35 +08:00
const config: BusterConfig = {
projects: [
{
name: 'project1',
data_source: 'postgres',
database: 'db',
schema: 'public',
},
],
};
2025-09-04 21:22:35 +08:00
await writeFile(join(testDir, 'buster.yml'), yaml.dump(config));
const result = await loadBusterConfig(testDir);
// Should return the config without logging (console.info calls were removed)
expect(result.config).toBeDefined();
expect(result.configPath).toContain('buster.yml');
expect(result.config.projects).toHaveLength(1);
});
});
});
describe('resolveConfiguration', () => {
2025-09-04 21:22:35 +08:00
const defaultConfig: BusterConfig = {
projects: [
{
2025-09-04 21:22:35 +08:00
name: 'test-project',
data_source: 'postgres',
2025-09-04 21:22:35 +08:00
database: 'test_db',
schema: 'public',
2025-09-04 21:22:35 +08:00
include: ['**/*.yml'],
exclude: ['**/temp/**'],
},
2025-09-04 21:22:35 +08:00
],
};
2025-09-04 21:22:35 +08:00
it('should resolve configuration with project name', () => {
const resolved = resolveConfiguration(defaultConfig, { dryRun: false }, 'test-project');
expect(resolved.data_source_name).toBe('postgres');
2025-09-04 21:22:35 +08:00
expect(resolved.database).toBe('test_db');
expect(resolved.schema).toBe('public');
2025-09-04 21:22:35 +08:00
expect(resolved.include).toEqual(['**/*.yml']);
expect(resolved.exclude).toEqual(['**/temp/**']);
});
2025-09-04 21:22:35 +08:00
it('should use first project when no name specified', () => {
const resolved = resolveConfiguration(defaultConfig, { dryRun: false });
2025-09-04 21:22:35 +08:00
expect(resolved.data_source_name).toBe('postgres');
});
2025-09-04 21:22:35 +08:00
it('should use default include patterns when not specified', () => {
const config: BusterConfig = {
projects: [
{
name: 'test-project',
data_source: 'postgres',
database: 'test_db',
schema: 'public',
},
],
};
const resolved = resolveConfiguration(config, { dryRun: false });
2025-09-04 21:22:35 +08:00
expect(resolved.include).toEqual(['**/*.yml', '**/*.yaml']);
expect(resolved.exclude).toEqual([]);
});
2025-09-04 21:22:35 +08:00
it('should throw error for non-existent project name', () => {
expect(() => resolveConfiguration(defaultConfig, { dryRun: false }, 'non-existent')).toThrow(
"Project 'non-existent' not found in buster.yml"
);
});
2025-09-04 21:22:35 +08:00
it('should throw error when no projects defined', () => {
const config: BusterConfig = {
projects: [],
};
2025-09-04 21:22:35 +08:00
expect(() => resolveConfiguration(config, { dryRun: false })).toThrow(
'No projects defined in buster.yml'
);
});
});
});