init
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* config.ts 단위 테스트
|
||||
* - getDefaultConfig()가 올바른 기본값을 반환하는지 확인
|
||||
* - 설정 로드/저장은 파일 I/O가 포함되므로 통합 테스트에 가까움
|
||||
*/
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import inquirer from 'inquirer';
|
||||
import { checkAndRunMigrations, CURRENT_CONFIG_VERSION } from '../src/migration';
|
||||
import {
|
||||
getDefaultConfig,
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
SKT_TERMS_URLS,
|
||||
SKT_TERMS_VERSION,
|
||||
validateRequiredFields,
|
||||
} from '../src/config';
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getDefaultConfig', () => {
|
||||
it('should return valid default config with required fields', () => {
|
||||
const config = getDefaultConfig();
|
||||
|
||||
expect(config._config_version).toBe(4);
|
||||
expect(config.credentials).toBeDefined();
|
||||
expect(config.terms).toEqual({
|
||||
provider: 'skt',
|
||||
accepted: false,
|
||||
accepted_at: '',
|
||||
version: SKT_TERMS_VERSION,
|
||||
urls: SKT_TERMS_URLS,
|
||||
});
|
||||
expect(config.plan.speed_mbps).toBe(1000);
|
||||
expect(config.schedule.timezone).toBe('Asia/Seoul');
|
||||
expect(config.headless).toBe(true);
|
||||
});
|
||||
|
||||
it('should have schedule with multi-attempt defaults', () => {
|
||||
const config = getDefaultConfig();
|
||||
|
||||
expect(config.schedule.max_attempts).toBeGreaterThan(1);
|
||||
expect(config.schedule.retry_interval_minutes).toBeGreaterThan(0);
|
||||
expect(config.schedule.stop_on_complaint_success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateRequiredFields', () => {
|
||||
it('should require SKT terms acceptance', () => {
|
||||
const config = getDefaultConfig();
|
||||
config.credentials.id = 'user@example.com';
|
||||
config.credentials.password = 'password';
|
||||
config.phone = '01012345678';
|
||||
|
||||
expect(validateRequiredFields(config)).toContain(
|
||||
'terms (SKT/SK브로드밴드 공식 이용약관 동의)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid SKT terms acceptance timestamps', () => {
|
||||
const config = getDefaultConfig();
|
||||
config.credentials.id = 'user@example.com';
|
||||
config.credentials.password = 'password';
|
||||
config.phone = '01012345678';
|
||||
config.terms = {
|
||||
provider: 'skt',
|
||||
accepted: true,
|
||||
accepted_at: 'not-a-date',
|
||||
version: SKT_TERMS_VERSION,
|
||||
urls: SKT_TERMS_URLS,
|
||||
};
|
||||
|
||||
expect(validateRequiredFields(config)).toContain(
|
||||
'terms (SKT/SK브로드밴드 공식 이용약관 동의)',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should reject normalized or underspecified timestamp strings', () => {
|
||||
const config = getDefaultConfig();
|
||||
config.credentials.id = 'user@example.com';
|
||||
config.credentials.password = 'password';
|
||||
config.phone = '01012345678';
|
||||
config.terms = {
|
||||
provider: 'skt',
|
||||
accepted: true,
|
||||
accepted_at: '1',
|
||||
version: SKT_TERMS_VERSION,
|
||||
urls: SKT_TERMS_URLS,
|
||||
};
|
||||
|
||||
expect(validateRequiredFields(config)).toContain(
|
||||
'terms (SKT/SK브로드밴드 공식 이용약관 동의)',
|
||||
);
|
||||
|
||||
config.terms.accepted_at = '2026-02-31T00:00:00.000Z';
|
||||
|
||||
expect(validateRequiredFields(config)).toContain(
|
||||
'terms (SKT/SK브로드밴드 공식 이용약관 동의)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept config with current SKT terms metadata', () => {
|
||||
const config = getDefaultConfig();
|
||||
config.credentials.id = 'user@example.com';
|
||||
config.credentials.password = 'password';
|
||||
config.phone = '01012345678';
|
||||
config.terms = {
|
||||
provider: 'skt',
|
||||
accepted: true,
|
||||
accepted_at: '2026-03-30T00:00:00.000Z',
|
||||
version: SKT_TERMS_VERSION,
|
||||
urls: SKT_TERMS_URLS,
|
||||
};
|
||||
|
||||
expect(validateRequiredFields(config)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadConfig and saveConfig terms handling', () => {
|
||||
it('should default missing terms in old configs safely', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'skt-config-'));
|
||||
const configPath = path.join(dir, 'config.yaml');
|
||||
fs.writeFileSync(configPath, [
|
||||
'_config_version: 3',
|
||||
'credentials:',
|
||||
' id: "user@example.com"',
|
||||
' password: "password"',
|
||||
'phone: "01012345678"',
|
||||
].join('\n'), 'utf8');
|
||||
|
||||
const config = loadConfig(configPath);
|
||||
|
||||
expect(config.terms.accepted).toBe(false);
|
||||
expect(config.terms.version).toBe(SKT_TERMS_VERSION);
|
||||
expect(config.terms.urls).toEqual(SKT_TERMS_URLS);
|
||||
});
|
||||
|
||||
it('should preserve accepted current terms when saving and loading', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'skt-config-'));
|
||||
const configPath = path.join(dir, 'config.yaml');
|
||||
const config = getDefaultConfig();
|
||||
config.terms = {
|
||||
provider: 'skt',
|
||||
accepted: true,
|
||||
accepted_at: '2026-03-30T00:00:00.000Z',
|
||||
version: SKT_TERMS_VERSION,
|
||||
urls: SKT_TERMS_URLS,
|
||||
};
|
||||
|
||||
saveConfig(config, configPath);
|
||||
const loaded = loadConfig(configPath);
|
||||
|
||||
expect(loaded.terms).toEqual(config.terms);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('terms migration', () => {
|
||||
it('should record SKT terms acceptance when migrating v3 configs to v4', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'skt-config-'));
|
||||
const configPath = path.join(dir, 'config.yaml');
|
||||
const config = getDefaultConfig();
|
||||
config._config_version = 3;
|
||||
config.credentials.id = 'user@example.com';
|
||||
config.credentials.password = 'password';
|
||||
config.phone = '01012345678';
|
||||
|
||||
vi.spyOn(inquirer, 'prompt').mockResolvedValue({ apply: true });
|
||||
|
||||
const migrated = await checkAndRunMigrations(config, configPath, { interactive: true });
|
||||
|
||||
expect(migrated._config_version).toBe(CURRENT_CONFIG_VERSION);
|
||||
expect(migrated.terms).toMatchObject({
|
||||
provider: 'skt',
|
||||
accepted: true,
|
||||
version: SKT_TERMS_VERSION,
|
||||
urls: SKT_TERMS_URLS,
|
||||
});
|
||||
expect(new Date(migrated.terms.accepted_at).toISOString()).toBe(migrated.terms.accepted_at);
|
||||
expect(loadConfig(configPath).terms.accepted).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* db.ts 단위 테스트
|
||||
* - getTodayRecords(): UTC로 저장된 measured_at과 타임존 로컬 날짜 비교가
|
||||
* 올바르게 동작해야 함 (이슈 #5)
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { SpeedDatabase, SpeedRecord } from '../src/db';
|
||||
|
||||
function makeRecord(overrides: Partial<SpeedRecord>): Omit<SpeedRecord, 'id'> {
|
||||
return {
|
||||
isp: 'skt',
|
||||
measured_at: new Date().toISOString(),
|
||||
download_mbps: 100,
|
||||
upload_mbps: 100,
|
||||
ping_ms: 10,
|
||||
sla_result: 'fail',
|
||||
complaint_filed: true,
|
||||
complaint_result: 'success',
|
||||
raw_data: '{}',
|
||||
error: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SpeedDatabase.getTodayRecords (timezone-aware)', () => {
|
||||
let tmpDir: string;
|
||||
let db: SpeedDatabase;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dmsk-db-'));
|
||||
db = new SpeedDatabase(path.join(tmpDir, 'test.db'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('treats UTC record from KST early morning as today (issue #5)', () => {
|
||||
// KST 2026-04-18 04:00 = UTC 2026-04-17 19:00
|
||||
const utcEarlyMorning = '2026-04-17T19:00:00.000Z';
|
||||
db.save(makeRecord({ measured_at: utcEarlyMorning }));
|
||||
|
||||
// 같은 KST 날짜(2026-04-18)의 06:00 = UTC 2026-04-17 21:00 시점 기준으로 조회
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-04-17T21:00:00.000Z'));
|
||||
try {
|
||||
const records = db.getTodayRecords('Asia/Seoul');
|
||||
expect(records).toHaveLength(1);
|
||||
expect(records[0].measured_at).toBe(utcEarlyMorning);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('hasTodayComplaintSuccess returns true for KST early-morning success', () => {
|
||||
db.save(
|
||||
makeRecord({
|
||||
measured_at: '2026-04-17T19:00:00.000Z', // KST 2026-04-18 04:00
|
||||
complaint_result: 'success',
|
||||
})
|
||||
);
|
||||
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-04-17T21:00:00.000Z')); // KST 06:00
|
||||
try {
|
||||
expect(db.hasTodayComplaintSuccess('Asia/Seoul')).toBe(true);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('excludes records from previous KST day', () => {
|
||||
// KST 2026-04-17 23:00 = UTC 2026-04-17 14:00 (어제)
|
||||
db.save(makeRecord({ measured_at: '2026-04-17T14:00:00.000Z' }));
|
||||
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-04-17T21:00:00.000Z')); // KST 2026-04-18 06:00
|
||||
try {
|
||||
const records = db.getTodayRecords('Asia/Seoul');
|
||||
expect(records).toHaveLength(0);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user