/**
* 設定セキュリティテスト(複数DB対応版)
*
* テスト対象: セキュリティ要件の検証
* - SEC-005: HTTPS通信強制
* - SEC-006: SSL証明書検証警告
* - パスワードマスキング(ログ出力時の安全性)
*
* 設計書 8.2 セクション準拠
* 実装計画書 Phase 1 準拠(複数DB対応)
*/
import {
CONFIG_DEFAULTS,
ENV_PREFIXES,
formatConfigForLog,
loadMultiDatabaseConfig,
validateDatabaseConfig,
type FileMakerConfig,
} from '../../../src/config.js';
import type { DatabaseConfig } from '../../../src/types/filemaker.js';
// ============================================================================
// SEC-005: HTTPS通信強制のテスト
// ============================================================================
describe('SEC-005: HTTPS通信強制', () => {
const originalEnv = process.env;
beforeEach(() => {
// テストごとに環境変数をリセット
jest.resetModules();
process.env = { ...originalEnv };
// 既存のFM_SERVER_* 環境変数をクリア
for (const key of Object.keys(process.env)) {
if (key.startsWith('FM_SERVER_') || key.startsWith('FM_DATABASE_') ||
key.startsWith('FM_ACCOUNT_') || key.startsWith('FM_PASSWORD_')) {
delete process.env[key];
}
}
});
afterAll(() => {
process.env = originalEnv;
});
describe('HTTPプロトコルの拒否', () => {
/**
* 前提条件: http://で始まるサーバーURLが設定されている
* 検証項目: 設定検証でエラーが返される
*/
it('http://で始まるURLはエラーとして拒否される', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'http://insecure-server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.valid).toBe(false);
expect(result.errors.some((e: string) => e.includes('HTTPS') || e.includes('SEC-005'))).toBe(true);
});
it('http://localhost もエラーとして拒否される', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'http://localhost',
database: 'TestDB',
username: 'user',
password: 'pass',
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.valid).toBe(false);
expect(result.errors.some((e: string) => e.includes('SEC-005'))).toBe(true);
});
it('http://127.0.0.1 もエラーとして拒否される', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'http://127.0.0.1',
database: 'TestDB',
username: 'user',
password: 'pass',
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.valid).toBe(false);
expect(result.errors.some((e: string) => e.includes('SEC-005'))).toBe(true);
});
});
describe('HTTPSプロトコルの受け入れ', () => {
/**
* 前提条件: https://で始まるサーバーURLが設定されている
* 検証項目: 設定検証が成功する
*/
it('https://で始まるURLは受け入れられる', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'https://secure-server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('https://localhost は受け入れられる', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'https://localhost',
database: 'TestDB',
username: 'user',
password: 'pass',
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.valid).toBe(true);
});
});
describe('URL正規化とHTTPS強制の連携', () => {
/**
* 前提条件: プロトコルなしのURLが環境変数に設定されている
* 検証項目: バリデーションエラーになる(新しいAPIでは自動補完しない)
*/
it('プロトコルなしのURLはエラーになる', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
};
const result = validateDatabaseConfig(config, 'TEST');
// 新しいAPIではhttps://で始まっていないURLはエラー
expect(result.valid).toBe(false);
});
});
});
// ============================================================================
// SEC-006: SSL証明書検証警告のテスト
// ============================================================================
describe('SEC-006: SSL証明書検証警告', () => {
describe('SSL検証無効化時の警告', () => {
/**
* 前提条件: sslVerify が false に設定されている
* 検証項目: 警告メッセージが出力される
*/
it('sslVerify=false の場合、警告が出力される', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'https://server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
sslVerify: false,
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.warnings.length).toBeGreaterThan(0);
expect(result.warnings.some((w: string) => w.includes('SEC-006'))).toBe(true);
expect(result.warnings.some((w: string) => w.includes('SSL certificate verification is disabled'))).toBe(true);
});
it('警告にもかかわらず設定自体は有効', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'https://server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
sslVerify: false,
};
const result = validateDatabaseConfig(config, 'TEST');
// 警告はあるが、エラーではないので valid: true
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
describe('SSL検証有効時の正常動作', () => {
/**
* 前提条件: sslVerify が true(デフォルト)に設定されている
* 検証項目: SEC-006警告は出力されない
*/
it('sslVerify=true の場合、SEC-006警告は出力されない', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'https://server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
sslVerify: true,
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.warnings.some((w: string) => w.includes('SEC-006'))).toBe(false);
});
it('デフォルト値 sslVerify=true が使用される', () => {
expect(CONFIG_DEFAULTS.sslVerify).toBe(true);
});
});
});
// ============================================================================
// パスワードマスキングのテスト
// ============================================================================
describe('パスワードマスキング', () => {
describe('formatConfigForLog', () => {
/**
* 前提条件: パスワードを含む設定オブジェクト
* 検証項目: パスワードがマスキングされる
*/
it('パスワードは ***MASKED*** にマスキングされる', () => {
const config: Partial<FileMakerConfig> = {
server: 'https://server.example.com',
database: 'TestDB',
username: 'admin',
password: 'SuperSecretPassword123!',
};
const formatted = formatConfigForLog(config);
expect(formatted.password).toBe('***MASKED***');
// 他のフィールドは保持される
expect(formatted.server).toBe('https://server.example.com');
expect(formatted.database).toBe('TestDB');
expect(formatted.username).toBe('admin');
});
it('パスワードがundefinedの場合はundefinedのまま', () => {
const config: Partial<FileMakerConfig> = {
server: 'https://server.example.com',
database: 'TestDB',
username: 'admin',
// password は undefined
};
const formatted = formatConfigForLog(config);
expect(formatted.password).toBeUndefined();
});
it('空文字のパスワードはマスキングされない(falsyなため)', () => {
const config: Partial<FileMakerConfig> = {
server: 'https://server.example.com',
database: 'TestDB',
username: 'admin',
password: '',
};
const formatted = formatConfigForLog(config);
// 空文字は falsy なので undefined が返される
expect(formatted.password).toBeUndefined();
});
it('DatabaseConfig形式でもマスキングが動作する', () => {
const config: Partial<DatabaseConfig> = {
alias: 'PRODUCTION',
server: 'https://server.example.com',
database: 'TestDB',
username: 'admin',
password: 'SuperSecretPassword123!',
};
const formatted = formatConfigForLog(config);
expect(formatted.password).toBe('***MASKED***');
expect(formatted.alias).toBe('PRODUCTION');
});
});
});
// ============================================================================
// 環境変数からの複数DB設定読み込みセキュリティ
// ============================================================================
describe('環境変数からの複数DB設定読み込みセキュリティ', () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
// 既存のFM_* 環境変数をクリア
for (const key of Object.keys(process.env)) {
if (key.startsWith('FM_SERVER_') || key.startsWith('FM_DATABASE_') ||
key.startsWith('FM_ACCOUNT_') || key.startsWith('FM_PASSWORD_') ||
key.startsWith('FM_SSL_VERIFY_') || key.startsWith('FM_API_VERSION_')) {
delete process.env[key];
}
}
});
afterAll(() => {
process.env = originalEnv;
});
describe('複数DB設定の読み込み', () => {
/**
* 前提条件: 複数のエイリアスで環境変数が設定されている
* 検証項目: 全て正しく読み込まれる
*/
it('有効な設定が読み込まれる', () => {
process.env[`${ENV_PREFIXES.server}PRODUCTION`] = 'https://prod.example.com';
process.env[`${ENV_PREFIXES.database}PRODUCTION`] = 'ProdDB';
process.env[`${ENV_PREFIXES.account}PRODUCTION`] = 'produser';
process.env[`${ENV_PREFIXES.password}PRODUCTION`] = 'prodpass';
const registry = loadMultiDatabaseConfig();
expect(registry.aliases).toContain('PRODUCTION');
expect(registry.databases.get('PRODUCTION')?.server).toBe('https://prod.example.com');
});
it('HTTPサーバーが設定されている場合はエラーをスローする', () => {
process.env[`${ENV_PREFIXES.server}INSECURE`] = 'http://insecure.example.com';
process.env[`${ENV_PREFIXES.database}INSECURE`] = 'InsecureDB';
process.env[`${ENV_PREFIXES.account}INSECURE`] = 'user';
process.env[`${ENV_PREFIXES.password}INSECURE`] = 'pass';
expect(() => loadMultiDatabaseConfig()).toThrow(/SEC-005/);
});
});
describe('エイリアス衝突検出', () => {
/**
* 前提条件: 大文字小文字違いで同じエイリアスが設定されている
* 検証項目: 衝突エラーがスローされる
*/
it('大文字小文字違いのエイリアス衝突を検出する', () => {
process.env[`${ENV_PREFIXES.server}Production`] = 'https://prod1.example.com';
process.env[`${ENV_PREFIXES.server}PRODUCTION`] = 'https://prod2.example.com';
process.env[`${ENV_PREFIXES.database}Production`] = 'DB1';
process.env[`${ENV_PREFIXES.database}PRODUCTION`] = 'DB2';
process.env[`${ENV_PREFIXES.account}Production`] = 'user1';
process.env[`${ENV_PREFIXES.account}PRODUCTION`] = 'user2';
process.env[`${ENV_PREFIXES.password}Production`] = 'pass1';
process.env[`${ENV_PREFIXES.password}PRODUCTION`] = 'pass2';
expect(() => loadMultiDatabaseConfig()).toThrow(/collision/i);
});
});
describe('設定が見つからない場合', () => {
/**
* 前提条件: FM_SERVER_* 環境変数が一つも設定されていない
* 検証項目: エラーがスローされる
*/
it('設定が見つからない場合はエラーをスローする', () => {
expect(() => loadMultiDatabaseConfig()).toThrow(/No database configuration found/);
});
});
});
// ============================================================================
// セッションタイムアウトのセキュリティテスト
// ============================================================================
describe('セッションタイムアウトのセキュリティ', () => {
describe('タイムアウト値の検証', () => {
/**
* 前提条件: 極端に短いまたは長いタイムアウト値
* 検証項目: 適切な警告が出力される
*/
it('60秒未満のタイムアウトには警告が出力される', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'https://server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
sessionTimeout: 30,
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.warnings.some((w: string) => w.includes('very short'))).toBe(true);
});
it('900秒超のタイムアウトには警告が出力される', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'https://server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
sessionTimeout: 1800,
};
const result = validateDatabaseConfig(config, 'TEST');
expect(result.warnings.some((w: string) => w.includes('exceeds FileMaker'))).toBe(true);
});
it('デフォルトタイムアウト(840秒)は警告なし', () => {
const config: Partial<DatabaseConfig> = {
alias: 'TEST',
server: 'https://server.example.com',
database: 'TestDB',
username: 'user',
password: 'pass',
sessionTimeout: CONFIG_DEFAULTS.sessionTimeout,
};
const result = validateDatabaseConfig(config, 'TEST');
// タイムアウト関連の警告がない
expect(result.warnings.some((w: string) => w.includes('short') || w.includes('exceeds'))).toBe(false);
});
});
});