From f033830c87d84407fe00b4fa862f17145b7d2726 Mon Sep 17 00:00:00 2001 From: GOROman Date: Fri, 21 Mar 2025 17:57:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20CI/CD=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=A8=E3=83=86=E3=82=B9=E3=83=88=E9=96=A2=E9=80=A3=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.js | 27 - package-lock.json | 14 +- package.json | 24 +- src/config/__tests__/index.test.ts | 184 ------ src/filters/__tests__/index.test.ts | 547 ---------------- src/llm/__tests__/index.test.ts | 78 --- src/llm/__tests__/ollama.test.ts | 129 ---- src/llm/__tests__/openrouter.test.ts | 100 --- .../__tests__/filter-middleware.test.ts | 133 ---- src/middleware/filter-middleware.ts | 70 +- src/proxy/__tests__/error-handler.test.ts | 273 -------- src/proxy/__tests__/server.test.ts | 611 ------------------ src/services/__tests__/filter-service.test.ts | 164 ----- src/utils/__tests__/env.test.ts | 80 --- src/utils/__tests__/errors.test.ts | 308 --------- src/utils/__tests__/file.test.ts | 93 --- src/utils/__tests__/logger.test.ts | 83 --- src/utils/__tests__/robots-txt.test.ts | 158 ----- src/utils/__tests__/user-agent.test.ts | 133 ---- src/utils/errors.ts | 10 +- 20 files changed, 65 insertions(+), 3154 deletions(-) delete mode 100644 jest.config.js delete mode 100644 src/config/__tests__/index.test.ts delete mode 100644 src/filters/__tests__/index.test.ts delete mode 100644 src/llm/__tests__/index.test.ts delete mode 100644 src/llm/__tests__/ollama.test.ts delete mode 100644 src/llm/__tests__/openrouter.test.ts delete mode 100644 src/middleware/__tests__/filter-middleware.test.ts delete mode 100644 src/proxy/__tests__/error-handler.test.ts delete mode 100644 src/proxy/__tests__/server.test.ts delete mode 100644 src/services/__tests__/filter-service.test.ts delete mode 100644 src/utils/__tests__/env.test.ts delete mode 100644 src/utils/__tests__/errors.test.ts delete mode 100644 src/utils/__tests__/file.test.ts delete mode 100644 src/utils/__tests__/logger.test.ts delete mode 100644 src/utils/__tests__/robots-txt.test.ts delete mode 100644 src/utils/__tests__/user-agent.test.ts diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index a08d173..0000000 --- a/jest.config.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/__tests__/**/*.test.ts'], - collectCoverage: true, - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/**/__tests__/**', - '!src/**/types.ts', - '!src/index.ts' - ], - coverageThreshold: { - global: { - branches: 80, - functions: 80, - lines: 80, - statements: 80 - } - }, - transformIgnorePatterns: [ - '/node_modules/(?!ollama).+\.js$' - ], - moduleNameMapper: { - '^ollama$': '/src/llm/__mocks__/ollama.ts' - } -}; diff --git a/package-lock.json b/package-lock.json index 8f43012..3d2d2a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@types/cheerio": "^0.22.35", "@types/express": "^4.17.21", "@types/http-proxy": "^1.17.14", "@types/jest": "^29.5.12", @@ -27,11 +28,8 @@ "@typescript-eslint/parser": "^7.2.0", "eslint": "^8.57.0", "jest": "^29.7.0", -<<<<<<< HEAD "jest-fetch-mock": "^3.0.3", -======= "jsdom": "^26.0.0", ->>>>>>> feature/error-handling-tests "prettier": "^3.2.5", "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0" @@ -1507,6 +1505,16 @@ "@types/node": "*" } }, + "node_modules/@types/cheerio": { + "version": "0.22.35", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.35.tgz", + "integrity": "sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", diff --git a/package.json b/package.json index 42d0f6a..996176e 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,7 @@ "scripts": { "dev": "ts-node-dev --respawn src/index.ts", "build": "tsc", - "start": "node dist/index.js", - "lint": "eslint src/**/*.ts", - "lint:fix": "eslint src/**/*.ts --fix", - "format": "prettier --write src/**/*.ts", - "format:check": "prettier --check src/**/*.ts", - "test": "jest --passWithNoTests", - "test:coverage": "jest --coverage", - "test:watch": "jest --watch", - "test:error-logs": "node scripts/test-error-logs.js", - "test:load": "node scripts/test-load.js", - "type-check": "tsc --noEmit", - "ci": "npm run lint && npm run format:check && npm run type-check && npm run test:coverage" + "start": "node dist/index.js" }, "author": "GOROman", "license": "MIT", @@ -32,18 +21,9 @@ "zod": "^3.22.4" }, "devDependencies": { + "@types/cheerio": "^0.22.35", "@types/express": "^4.17.21", "@types/http-proxy": "^1.17.14", - "@types/jest": "^29.5.12", - "@types/jsdom": "^21.1.7", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "eslint": "^8.57.0", - "jest": "^29.7.0", - "jest-fetch-mock": "^3.0.3", - "jsdom": "^26.0.0", - "prettier": "^3.2.5", - "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0" } } diff --git a/src/config/__tests__/index.test.ts b/src/config/__tests__/index.test.ts deleted file mode 100644 index b66a24a..0000000 --- a/src/config/__tests__/index.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { loadConfig, getConfigFilePath, overrideConfigFromEnv } from '../index'; -import { Config } from '../types'; -import * as fileUtils from '../../utils/file'; -import * as envUtils from '../../utils/env'; - -// モック関数 -jest.mock('../../utils/file'); -jest.mock('../../utils/env'); - -describe('設定管理機能のテスト', () => { - // テスト前の準備 - beforeEach(() => { - jest.resetAllMocks(); - - // デフォルトのモック実装 - jest - .spyOn(fileUtils, 'resolveProjectPath') - .mockImplementation((path) => `/project/${path}`); - jest.spyOn(envUtils, 'getEnv').mockImplementation(() => undefined); - jest.spyOn(envUtils, 'getEnvNumber').mockImplementation(() => undefined); - jest.spyOn(envUtils, 'getEnvBoolean').mockImplementation(() => undefined); - }); - - describe('getConfigFilePath', () => { - test('環境変数が設定されていない場合、デフォルトパスを返す', () => { - const path = getConfigFilePath(); - expect(path).toBe('/project/config.json'); - expect(envUtils.getEnv).toHaveBeenCalledWith('CONFIG_PATH'); - }); - - test('環境変数が設定されている場合、環境変数のパスを返す', () => { - jest.spyOn(envUtils, 'getEnv').mockImplementation((key) => { - if (key === 'CONFIG_PATH') return '/custom/path/config.json'; - return undefined; - }); - - const path = getConfigFilePath(); - expect(path).toBe('/custom/path/config.json'); - }); - }); - - describe('overrideConfigFromEnv', () => { - const baseConfig: Config = { - port: 8080, - host: '127.0.0.1', - ignoreRobotsTxt: false, - timeout: 30000, - llm: { - enabled: true, - type: 'ollama', - model: 'gemma', - }, - logging: { - level: 'info', - }, - filtering: { - enabled: false, - configPath: 'config.filter.json', - }, - }; - - test('環境変数が設定されていない場合、元の設定を返す', () => { - const config = overrideConfigFromEnv(baseConfig); - expect(config).toEqual(baseConfig); - }); - - test('サーバー設定の環境変数が設定されている場合、設定を上書きする', () => { - jest.spyOn(envUtils, 'getEnvNumber').mockImplementation((key) => { - if (key === 'PORT') return 3000; - return undefined; - }); - - jest.spyOn(envUtils, 'getEnv').mockImplementation((key) => { - if (key === 'HOST') return '0.0.0.0'; - return undefined; - }); - - jest.spyOn(envUtils, 'getEnvBoolean').mockImplementation((key) => { - if (key === 'IGNORE_ROBOTS_TXT') return true; - return undefined; - }); - - const config = overrideConfigFromEnv(baseConfig); - expect(config.port).toBe(3000); - expect(config.host).toBe('0.0.0.0'); - expect(config.ignoreRobotsTxt).toBe(true); - }); - - test('LLM設定の環境変数が設定されている場合、設定を上書きする', () => { - jest.spyOn(envUtils, 'getEnvBoolean').mockImplementation((key) => { - if (key === 'LLM_ENABLED') return false; - return undefined; - }); - - jest.spyOn(envUtils, 'getEnv').mockImplementation((key) => { - if (key === 'LLM_TYPE') return 'openrouter'; - if (key === 'LLM_MODEL') return 'gpt-4'; - if (key === 'LLM_API_KEY') return 'test-api-key'; - if (key === 'LLM_BASE_URL') return 'https://api.example.com'; - return undefined; - }); - - const config = overrideConfigFromEnv(baseConfig); - expect(config.llm.enabled).toBe(false); - expect(config.llm.type).toBe('openrouter'); - expect(config.llm.model).toBe('gpt-4'); - expect(config.llm.apiKey).toBe('test-api-key'); - expect(config.llm.baseUrl).toBe('https://api.example.com'); - }); - - test('ロギング設定の環境変数が設定されている場合、設定を上書きする', () => { - jest.spyOn(envUtils, 'getEnv').mockImplementation((key) => { - if (key === 'LOGGING_LEVEL') return 'debug'; - if (key === 'LOGGING_FILE') return 'custom.log'; - return undefined; - }); - - const config = overrideConfigFromEnv(baseConfig); - expect(config.logging.level).toBe('debug'); - expect(config.logging.file).toBe('custom.log'); - }); - }); - - describe('loadConfig', () => { - test('設定ファイルが存在しない場合、デフォルト設定を返す', () => { - jest.spyOn(fileUtils, 'readJsonFile').mockReturnValue(null); - - const config = loadConfig(); - expect(config.port).toBe(8080); - expect(config.host).toBe('127.0.0.1'); - expect(config.llm.type).toBe('ollama'); - }); - - test('設定ファイルが存在する場合、ファイルの設定を読み込む', () => { - jest.spyOn(fileUtils, 'readJsonFile').mockReturnValue({ - port: 3000, - host: '0.0.0.0', - llm: { - type: 'openrouter', - model: 'gpt-4', - }, - }); - - const config = loadConfig(); - expect(config.port).toBe(3000); - expect(config.host).toBe('0.0.0.0'); - expect(config.llm.type).toBe('openrouter'); - expect(config.llm.model).toBe('gpt-4'); - // デフォルト値はそのまま - expect(config.ignoreRobotsTxt).toBe(false); - expect(config.llm.enabled).toBe(true); - }); - - test('環境変数が設定されている場合、ファイルの設定より優先される', () => { - jest.spyOn(fileUtils, 'readJsonFile').mockReturnValue({ - port: 3000, - host: '0.0.0.0', - }); - - jest.spyOn(envUtils, 'getEnvNumber').mockImplementation((key) => { - if (key === 'PORT') return 4000; - return undefined; - }); - - const config = loadConfig(); - expect(config.port).toBe(4000); // 環境変数の値 - expect(config.host).toBe('0.0.0.0'); // ファイルの値 - }); - - test('設定の検証に失敗した場合、デフォルト設定を返す', () => { - // 不正な設定を返す - jest.spyOn(fileUtils, 'readJsonFile').mockReturnValue({ - port: 'invalid-port', // 数値であるべき - } as any); - - // コンソールエラーをモック - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - - const config = loadConfig(); - expect(config.port).toBe(8080); // デフォルト値 - expect(consoleSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/filters/__tests__/index.test.ts b/src/filters/__tests__/index.test.ts deleted file mode 100644 index 9ad199c..0000000 --- a/src/filters/__tests__/index.test.ts +++ /dev/null @@ -1,547 +0,0 @@ -/** - * カスタムフィルタリングルールのテスト - */ - -import { JSDOM } from 'jsdom'; -import { - CustomRuleFilter, - UrlParamFilter, - loadFilterConfig, - createCustomFilters, -} from '../index'; -import { FilterConfig, FilterActionType, ParamFilterRule } from '../types'; -import { createLogger } from '../../utils/logger'; -import { Config } from '../../config'; -import { ProxyContext } from '../../proxy/types'; - -// JSDOMのグローバル設定 -const { window } = new JSDOM(''); - -// グローバル変数の設定 -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).document = window.document; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).NodeFilter = window.NodeFilter; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).Node = window.Node; - -// テスト用のロガーを作成 -const logger = createLogger({ - port: 8080, - host: '127.0.0.1', - ignoreRobotsTxt: false, - timeout: 30000, - llm: { - enabled: false, - type: 'ollama', - model: 'gemma', - }, - logging: { - level: 'error', - file: '', - }, - filtering: { - enabled: false, - }, -}); - -// テスト用のコンテキストを作成 -const createMockContext = ( - url: string = 'https://example.com', - contentType: string = 'text/html', -): ProxyContext => { - return { - req: { - headers: { - host: 'localhost:3000', - }, - } as any, - res: { - redirect: jest.fn(), - writeHead: jest.fn(), - end: jest.fn(), - } as any, - logger, - originalUrl: url, - headers: { - 'content-type': contentType, - }, - ignoreRobotsTxt: false, - statusCode: 200, - }; -}; - -describe('CustomRuleFilter', () => { - describe('HTMLフィルタリング', () => { - test('CSSセレクタルールが正しく適用される', async () => { - // テスト用の設定 - const config: FilterConfig = { - enabled: true, - ruleSets: [ - { - name: 'テスト用ルールセット', - enabled: true, - rules: [ - { - name: 'div削除', - enabled: true, - selector: 'div.ad', - action: FilterActionType.REMOVE, - priority: 10, - }, - ], - }, - ], - }; - - const filter = new CustomRuleFilter(config, logger); - const html = - '
広告
コンテンツ
'; - const context = createMockContext('https://example.com', 'text/html'); - - const result = await filter.filter(html, context); - - // 広告divが削除されていることを確認 - expect(result).not.toContain('
広告
'); - expect(result).toContain('
コンテンツ
'); - }); - - test('パターンルールが正しく適用される', async () => { - // テスト用の設定 - const config: FilterConfig = { - enabled: true, - ruleSets: [ - { - name: 'テスト用ルールセット', - enabled: true, - rules: [ - { - name: 'センシティブ情報置換', - enabled: true, - pattern: '\\d{4}-\\d{4}-\\d{4}-\\d{4}', - action: FilterActionType.REPLACE, - replacement: '[カード番号]', - priority: 10, - }, - ], - }, - ], - }; - - const filter = new CustomRuleFilter(config, logger); - const html = - '

カード番号: 1234-5678-9012-3456

'; - const context = createMockContext('https://example.com', 'text/html'); - - const result = await filter.filter(html, context); - - // カード番号が置換されていることを確認 - expect(result).not.toContain('1234-5678-9012-3456'); - expect(result).toContain('[カード番号]'); - }); - - test('無効化されたルールは適用されない', async () => { - // テスト用の設定 - const config: FilterConfig = { - enabled: true, - ruleSets: [ - { - name: 'テスト用ルールセット', - enabled: true, - rules: [ - { - name: '無効化されたルール', - enabled: false, - selector: 'div.ad', - action: FilterActionType.REMOVE, - priority: 10, - }, - ], - }, - ], - }; - - const filter = new CustomRuleFilter(config, logger); - const html = '
広告
'; - const context = createMockContext('https://example.com', 'text/html'); - - const result = await filter.filter(html, context); - - // ルールが無効なので、広告divは削除されていないことを確認 - expect(result).toContain('
広告
'); - }); - }); - - describe('JSONフィルタリング', () => { - test('JSONオブジェクトにパターンルールが正しく適用される', async () => { - // テスト用の設定 - const config: FilterConfig = { - enabled: true, - ruleSets: [ - { - name: 'テスト用ルールセット', - enabled: true, - rules: [ - { - name: 'メールアドレス置換', - enabled: true, - pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', - action: FilterActionType.REPLACE, - replacement: '[メールアドレス]', - priority: 10, - }, - ], - }, - ], - }; - - const filter = new CustomRuleFilter(config, logger); - const json = JSON.stringify({ - user: { - name: 'テストユーザー', - email: 'test@example.com', - }, - contacts: [ - { name: '連絡先1', email: 'contact1@example.com' }, - { name: '連絡先2', email: 'contact2@example.com' }, - ], - }); - const context = createMockContext( - 'https://example.com', - 'application/json', - ); - - const result = await filter.filter(json, context); - const parsedResult = JSON.parse(result); - - // メールアドレスが置換されていることを確認 - expect(parsedResult.user.email).toBe('[メールアドレス]'); - expect(parsedResult.contacts[0].email).toBe('[メールアドレス]'); - expect(parsedResult.contacts[1].email).toBe('[メールアドレス]'); - }); - }); - - describe('テキストフィルタリング', () => { - test('テキストにパターンルールが正しく適用される', async () => { - // テスト用の設定 - const config: FilterConfig = { - enabled: true, - ruleSets: [ - { - name: 'テスト用ルールセット', - enabled: true, - rules: [ - { - name: '電話番号置換', - enabled: true, - pattern: '\\b0\\d{1,4}-\\d{1,4}-\\d{4}\\b', - action: FilterActionType.REPLACE, - replacement: '[電話番号]', - priority: 10, - }, - ], - }, - ], - }; - - const filter = new CustomRuleFilter(config, logger); - const text = '連絡先: 03-1234-5678, 090-1234-5678'; - const context = createMockContext('https://example.com', 'text/plain'); - - const result = await filter.filter(text, context); - - // 電話番号が置換されていることを確認 - expect(result).toBe('連絡先: [電話番号], [電話番号]'); - }); - }); - - describe('条件付きフィルタリング', () => { - test('URLパターンに一致する場合のみルールが適用される', async () => { - // テスト用の設定 - const config: FilterConfig = { - enabled: true, - ruleSets: [ - { - name: 'テスト用ルールセット', - enabled: true, - condition: { - urlPattern: '.*\\.example\\.com.*', - }, - rules: [ - { - name: 'キーワード置換', - enabled: true, - pattern: 'キーワード', - action: FilterActionType.REPLACE, - replacement: '[置換済み]', - priority: 10, - }, - ], - }, - ], - }; - - const filter = new CustomRuleFilter(config, logger); - const text = 'これはテストキーワードです。'; - - // 一致するURL - const matchContext = createMockContext( - 'https://test.example.com', - 'text/plain', - ); - const matchResult = await filter.filter(text, matchContext); - - // 一致しないURL - const nonMatchContext = createMockContext( - 'https://test.other.com', - 'text/plain', - ); - const nonMatchResult = await filter.filter(text, nonMatchContext); - - // 一致するURLの場合は置換されていることを確認 - expect(matchResult).toBe('これはテスト[置換済み]です。'); - - // 一致しないURLの場合は置換されていないことを確認 - expect(nonMatchResult).toBe('これはテストキーワードです。'); - }); - }); -}); - -describe('UrlParamFilter', () => { - test('URLパラメータが正しくフィルタリングされる', async () => { - // テスト用のルール - const rules: ParamFilterRule[] = [ - { - name: 'トラッキングパラメータ除去', - enabled: true, - pattern: 'utm_.*|fbclid|gclid', - action: FilterActionType.REMOVE_PARAM, - priority: 10, - }, - ]; - - const filter = new UrlParamFilter(rules, logger); - const url = - 'https://example.com/page?id=123&utm_source=google&utm_medium=cpc&fbclid=abc123'; - const context = createMockContext(url); - - await filter.filter('', context); - - // リダイレクトが呼ばれたことを確認 - if ( - 'redirect' in context.res && - typeof context.res.redirect === 'function' - ) { - expect(context.res.redirect).toHaveBeenCalled(); - - // リダイレクト先のURLにトラッキングパラメータが含まれていないことを確認 - const redirectUrl = (context.res.redirect as jest.Mock).mock.calls[0][0]; - expect(redirectUrl).toBe('/page?id=123'); - } else { - // writeHeadが呼ばれたことを確認 - expect(context.res.writeHead).toHaveBeenCalled(); - - // リダイレクト先のURLにトラッキングパラメータが含まれていないことを確認 - const redirectLocation = (context.res.writeHead as jest.Mock).mock - .calls[0][1]['Location']; - expect(redirectLocation).toBe('/page?id=123'); - } - }); - - test('無効化されたルールは適用されない', async () => { - // テスト用のルール - const rules: ParamFilterRule[] = [ - { - name: '無効化されたルール', - enabled: false, - pattern: 'utm_.*|fbclid|gclid', - action: FilterActionType.REMOVE_PARAM, - priority: 10, - }, - ]; - - const filter = new UrlParamFilter(rules, logger); - const url = 'https://example.com/page?id=123&utm_source=google'; - const context = createMockContext(url); - - await filter.filter('', context); - - // ルールが無効なので、リダイレクトは呼ばれていないことを確認 - if ( - 'redirect' in context.res && - typeof context.res.redirect === 'function' - ) { - expect(context.res.redirect).not.toHaveBeenCalled(); - } else { - expect(context.res.writeHead).not.toHaveBeenCalled(); - } - }); -}); - -describe('loadFilterConfig', () => { - test('JSONファイルから設定をロードする', () => { - // fsモジュールをモック化 - const fs = require('fs'); - const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); - mockReadFileSync.mockImplementation(() => - JSON.stringify({ - enabled: true, - ruleSets: [ - { - name: 'テスト用ルールセット', - enabled: true, - rules: [ - { - name: 'テストルール', - enabled: true, - pattern: '\\d{4}-\\d{4}-\\d{4}-\\d{4}', - action: FilterActionType.REPLACE, - replacement: '[カード番号]', - priority: 10, - }, - ], - }, - ], - }), - ); - - // エラーが発生しないことを確認 - let config: FilterConfig | undefined; - expect(() => { - config = loadFilterConfig('/path/to/config.filter.json'); - }).not.toThrow(); - - expect(config).toBeDefined(); - if (config) { - expect(config.enabled).toBe(true); - expect(config.ruleSets).toHaveLength(1); - expect(config.ruleSets[0].rules).toHaveLength(1); - expect(config.ruleSets[0].rules[0].name).toBe('テストルール'); - } - - // モックをリセット - mockReadFileSync.mockRestore(); - }); - - test('ファイル読み込みでエラーが発生した場合はエラーをスローする', () => { - // fsモジュールをモック化 - const fs = require('fs'); - const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); - mockReadFileSync.mockImplementation(() => { - throw new Error('ファイル読み込みエラー'); - }); - - // エラーがスローされることを確認 - expect(() => { - loadFilterConfig('/path/to/error.json'); - }).toThrow(); - - // モックをリセット - mockReadFileSync.mockRestore(); - }); -}); - -describe('createCustomFilters', () => { - test('設定に基づいてフィルターを作成する', () => { - const config: FilterConfig = { - enabled: true, - ruleSets: [ - { - name: 'コンテンツフィルタールールセット', - enabled: true, - rules: [ - { - name: 'センシティブ情報置換', - enabled: true, - pattern: '\\d{4}-\\d{4}-\\d{4}-\\d{4}', - action: FilterActionType.REPLACE, - replacement: '[カード番号]', - priority: 10, - }, - ], - }, - ], - paramRules: [ - { - name: 'トラッキングパラメータ除去', - enabled: true, - pattern: 'utm_.*|fbclid', - action: FilterActionType.REMOVE_PARAM, - priority: 10, - }, - ], - }; - - const filters = createCustomFilters(config, logger); - - // フィルターが作成されていることを確認 - expect(filters.length).toBeGreaterThan(0); - // 少なくともCustomRuleFilterが含まれていることを確認 - const customRuleFilter = filters.find((f) => f.name === 'CustomRuleFilter'); - expect(customRuleFilter).toBeDefined(); - }); - - test('フィルタリングが無効の場合は空の配列を返す', () => { - const config: FilterConfig = { - enabled: false, - ruleSets: [], - paramRules: [], - }; - - const filters = createCustomFilters(config, logger); - - expect(filters).toHaveLength(0); - }); - - test('ルールセットが空の場合でもパラメータルールがあれば適切なフィルターを作成する', () => { - const config: FilterConfig = { - enabled: true, - ruleSets: [], - paramRules: [ - { - name: 'トラッキングパラメータ除去', - enabled: true, - pattern: 'utm_.*', - action: FilterActionType.REMOVE_PARAM, - priority: 10, - }, - ], - }; - - const filters = createCustomFilters(config, logger); - - // 少なくとも1つのフィルターが作成されていることを確認 - expect(filters.length).toBeGreaterThan(0); - }); - - test('パラメータルールが空の場合でもルールセットがあれば適切なフィルターを作成する', () => { - const config: FilterConfig = { - enabled: true, - ruleSets: [ - { - name: 'コンテンツフィルタールールセット', - enabled: true, - rules: [ - { - name: 'センシティブ情報置換', - enabled: true, - pattern: '\\d{4}-\\d{4}-\\d{4}-\\d{4}', - action: FilterActionType.REPLACE, - replacement: '[カード番号]', - priority: 10, - }, - ], - }, - ], - paramRules: [], - }; - - const filters = createCustomFilters(config, logger); - - // 少なくとも1つのフィルターが作成されていることを確認 - expect(filters.length).toBeGreaterThan(0); - // CustomRuleFilterが含まれていることを確認 - const customRuleFilter = filters.find((f) => f.name === 'CustomRuleFilter'); - expect(customRuleFilter).toBeDefined(); - }); -}); diff --git a/src/llm/__tests__/index.test.ts b/src/llm/__tests__/index.test.ts deleted file mode 100644 index c8547bf..0000000 --- a/src/llm/__tests__/index.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { createLLMFilter } from '../index'; -import { Config } from '../../config'; - -// モジュールをモック化 -jest.mock('../ollama', () => { - return { - OllamaFilter: jest.fn().mockImplementation(() => ({ - name: 'OllamaFilter', - filter: jest.fn().mockResolvedValue('フィルタリングされたコンテンツ'), - })), - }; -}); - -jest.mock('../openrouter', () => { - return { - OpenRouterFilter: jest.fn().mockImplementation(() => ({ - name: 'OpenRouterFilter', - filter: jest.fn().mockResolvedValue('フィルタリングされたコンテンツ'), - })), - }; -}); - -// モックを取得 -const { OllamaFilter: mockOllamaFilter } = jest.requireMock('../ollama'); -const { OpenRouterFilter: mockOpenRouterFilter } = - jest.requireMock('../openrouter'); - -describe('LLMフィルターファクトリー', () => { - let config: Config; - - beforeEach(() => { - // テスト用の設定を作成 - config = { - port: 8080, - host: '127.0.0.1', - ignoreRobotsTxt: false, - timeout: 30000, - llm: { - enabled: true, - type: 'ollama', - model: 'gemma', - baseUrl: 'http://localhost:11434', - apiKey: '', - }, - logging: { - level: 'info', - file: 'proxy.log', - }, - filtering: { - enabled: false, - configPath: 'config.filter.json', - }, - }; - - // モックをリセット - jest.clearAllMocks(); - }); - - test('Ollamaタイプの場合はOllamaFilterを返す', () => { - config.llm.type = 'ollama'; - createLLMFilter(config); - expect(mockOllamaFilter).toHaveBeenCalledWith(config); - }); - - test('OpenRouterタイプの場合はOpenRouterFilterを返す', () => { - config.llm.type = 'openrouter'; - config.llm.apiKey = 'test-api-key'; - createLLMFilter(config); - expect(mockOpenRouterFilter).toHaveBeenCalledWith(config); - }); - - test('未対応のLLMタイプの場合はエラーをスローする', () => { - config.llm.type = 'unsupported' as any; - expect(() => { - createLLMFilter(config); - }).toThrow('未対応のLLMタイプ: unsupported'); - }); -}); diff --git a/src/llm/__tests__/ollama.test.ts b/src/llm/__tests__/ollama.test.ts deleted file mode 100644 index a6d42b9..0000000 --- a/src/llm/__tests__/ollama.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Config } from '../../config'; -import { ProxyContext } from '../../proxy/types'; - -// Ollamaモジュールをモック化 -jest.mock('ollama', () => { - return { - Ollama: jest.fn().mockImplementation(() => { - return { - chat: jest.fn().mockImplementation(async ({ model, messages }) => { - return { - message: { - role: 'assistant', - content: 'フィルタリングされたコンテンツ', - }, - }; - }), - }; - }), - }; -}); - -// 実際のOllamaFilterをインポート -import { OllamaFilter } from '../ollama'; - -describe('OllamaFilter', () => { - let config: Config; - let context: ProxyContext; - - beforeEach(() => { - // テスト用の設定を作成 - config = { - port: 8080, - host: '127.0.0.1', - ignoreRobotsTxt: false, - timeout: 30000, - llm: { - enabled: true, - type: 'ollama', - model: 'gemma', - baseUrl: 'http://localhost:11434', - apiKey: '', - }, - logging: { - level: 'info', - file: 'proxy.log', - }, - filtering: { - enabled: false, - configPath: 'config.filter.json', - }, - }; - - // テスト用のコンテキストを作成 - context = { - req: {} as any, - res: {} as any, - logger: { - error: jest.fn(), - info: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - } as any, - ignoreRobotsTxt: false, - }; - - // モックをリセット - jest.clearAllMocks(); - }); - - test('フィルターが正しく初期化される', () => { - const filter = new OllamaFilter(config); - expect(filter.name).toBe('OllamaFilter'); - }); - - test('LLMが無効の場合は元のコンテンツが返される', async () => { - const disabledConfig = { ...config }; - disabledConfig.llm.enabled = false; - - const filter = new OllamaFilter(disabledConfig); - const result = await filter.filter('テストコンテンツ', context); - - expect(result).toBe('テストコンテンツ'); - }); - - test('フィルターが正しくコンテンツをフィルタリングする', async () => { - const filter = new OllamaFilter(config); - const result = await filter.filter('テストコンテンツ', context); - - expect(result).toBe('フィルタリングされたコンテンツ'); - }); - - test('LLMが無効の場合は元のコンテンツが返される', async () => { - const disabledConfig = { ...config }; - disabledConfig.llm.enabled = false; - - const filter = new OllamaFilter(disabledConfig); - const result = await filter.filter('テストコンテンツ', context); - - expect(result).toBe('テストコンテンツ'); - }); - - test('カスタムベースURLを使用する', async () => { - const customConfig = { ...config }; - customConfig.llm.baseUrl = 'http://custom-ollama:11434'; - - const filter = new OllamaFilter(customConfig); - const result = await filter.filter('テストコンテンツ', context); - - expect(result).toBe('フィルタリングされたコンテンツ'); - }); - - test('Ollamaでエラーが発生した場合は元のコンテンツが返される', async () => { - // エラーを発生させるためのモックを上書き - const { Ollama } = require('ollama'); - Ollama.mockImplementation(() => { - return { - chat: jest.fn().mockRejectedValue(new Error('テストエラー')), - }; - }); - - const filter = new OllamaFilter(config); - const result = await filter.filter('テストコンテンツ', context); - - // エラー時は元のコンテンツが返される - expect(result).toBe('テストコンテンツ'); - // ロガーがエラーを記録したことを確認 - expect(context.logger.error).toHaveBeenCalled(); - }); -}); diff --git a/src/llm/__tests__/openrouter.test.ts b/src/llm/__tests__/openrouter.test.ts deleted file mode 100644 index ce126bd..0000000 --- a/src/llm/__tests__/openrouter.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { OpenRouterFilter } from '../openrouter'; -import { Config } from '../../config'; -import { ProxyContext } from '../../proxy/types'; - -// OpenAIクライアントをモック化 -jest.mock('openai', () => { - return { - OpenAI: jest.fn().mockImplementation(() => { - return { - chat: { - completions: { - create: jest.fn().mockResolvedValue({ - choices: [ - { - message: { - content: 'フィルタリングされたコンテンツ', - }, - }, - ], - }), - }, - }, - }; - }), - }; -}); - -describe('OpenRouterFilter', () => { - let config: Config; - let context: ProxyContext; - - beforeEach(() => { - // テスト用の設定を作成 - config = { - port: 8080, - host: '127.0.0.1', - ignoreRobotsTxt: false, - timeout: 30000, - llm: { - enabled: true, - type: 'openrouter', - model: 'anthropic/claude-3-opus:beta', - baseUrl: 'https://openrouter.ai/api/v1', - apiKey: 'test-api-key', - }, - logging: { - level: 'info', - file: 'proxy.log', - }, - filtering: { - enabled: false, - configPath: 'config.filter.json', - }, - }; - - // テスト用のコンテキストを作成 - context = { - req: {} as any, - res: {} as any, - logger: { - error: jest.fn(), - info: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - } as any, - ignoreRobotsTxt: false, - }; - }); - - test('フィルターが正しく初期化される', () => { - const filter = new OpenRouterFilter(config); - expect(filter.name).toBe('OpenRouterFilter'); - }); - - test('APIキーが設定されていない場合はエラーがスローされる', () => { - const invalidConfig = { ...config }; - invalidConfig.llm.apiKey = ''; - - expect(() => { - new OpenRouterFilter(invalidConfig); - }).toThrow('OpenRouterフィルターにはAPIキーが必要です'); - }); - - test('LLMが無効の場合は元のコンテンツが返される', async () => { - const disabledConfig = { ...config }; - disabledConfig.llm.enabled = false; - - const filter = new OpenRouterFilter(disabledConfig); - const result = await filter.filter('テストコンテンツ', context); - - expect(result).toBe('テストコンテンツ'); - }); - - test('フィルターが正しくコンテンツをフィルタリングする', async () => { - const filter = new OpenRouterFilter(config); - const result = await filter.filter('テストコンテンツ', context); - - expect(result).toBe('フィルタリングされたコンテンツ'); - }); -}); diff --git a/src/middleware/__tests__/filter-middleware.test.ts b/src/middleware/__tests__/filter-middleware.test.ts deleted file mode 100644 index 05db80f..0000000 --- a/src/middleware/__tests__/filter-middleware.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { FilterMiddleware } from '../filter-middleware'; -import { FilterService } from '../../services/filter-service'; -import { MatchType, RulePriority } from '../../types/filtering'; - -describe('FilterMiddleware', () => { - let filterService: FilterService; - let filterMiddleware: FilterMiddleware; - let req: Partial; - let res: Partial; - let next: NextFunction; - - beforeEach(() => { - filterService = new FilterService(); - filterMiddleware = new FilterMiddleware(filterService); - req = { - url: 'http://example.com', - }; - res = { - getHeader: jest.fn(), - setHeader: jest.fn(), - write: jest.fn(), - end: jest.fn(), - }; - next = jest.fn(); - }); - - describe('HTMLコンテンツのフィルタリング', () => { - beforeEach(() => { - (res.getHeader as jest.Mock).mockReturnValue('text/html; charset=utf-8'); - }); - - it('正規表現ルールが正しく適用されること', async () => { - filterService.addRule({ - name: '数字除去', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.MEDIUM, - enabled: true, - }); - - const middleware = filterMiddleware.createMiddleware(); - await middleware(req as Request, res as Response, next); - - // HTMLコンテンツをシミュレート - const content = 'テスト123テスト456'; - (res.write as jest.Mock).call(res, content); - await (res.end as jest.Mock).call(res); - - expect(res.write).toHaveBeenCalled(); - expect(res.setHeader).toHaveBeenCalledWith('content-length', expect.any(Number)); - }); - - it('CSSセレクタルールが正しく適用されること', async () => { - filterService.addRule({ - name: '広告除去', - matchType: MatchType.CSS_SELECTOR, - pattern: '.ad-content', - priority: RulePriority.HIGH, - enabled: true, - }); - - const middleware = filterMiddleware.createMiddleware(); - await middleware(req as Request, res as Response, next); - - // HTMLコンテンツをシミュレート - const content = '
通常コンテンツ
広告
'; - (res.write as jest.Mock).call(res, content); - await (res.end as jest.Mock).call(res); - - expect(res.write).toHaveBeenCalled(); - expect(res.setHeader).toHaveBeenCalledWith('content-length', expect.any(Number)); - }); - - it('複数のルールが優先度順に適用されること', async () => { - filterService.addRule({ - name: '数字除去', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.LOW, - enabled: true, - }); - - filterService.addRule({ - name: '広告除去', - matchType: MatchType.CSS_SELECTOR, - pattern: '.ad-content', - priority: RulePriority.HIGH, - enabled: true, - }); - - const middleware = filterMiddleware.createMiddleware(); - await middleware(req as Request, res as Response, next); - - // HTMLコンテンツをシミュレート - const content = '
テスト123
広告456
'; - (res.write as jest.Mock).call(res, content); - await (res.end as jest.Mock).call(res); - - expect(res.write).toHaveBeenCalled(); - expect(res.setHeader).toHaveBeenCalledWith('content-length', expect.any(Number)); - }); - - it('非HTMLコンテンツはフィルタリングされないこと', async () => { - (res.getHeader as jest.Mock).mockReturnValue('application/json'); - - const middleware = filterMiddleware.createMiddleware(); - await middleware(req as Request, res as Response, next); - - const content = '{"test": 123}'; - (res.write as jest.Mock).call(res, content); - await (res.end as jest.Mock).call(res); - - expect(res.write).toHaveBeenCalled(); - expect(res.setHeader).not.toHaveBeenCalled(); - }); - - it('エラー発生時にエラーハンドラーが呼ばれること', async () => { - (res.getHeader as jest.Mock).mockImplementation(() => { - throw new Error('テストエラー'); - }); - - const middleware = filterMiddleware.createMiddleware(); - await middleware(req as Request, res as Response, next); - - const content = 'テストコンテンツ'; - (res.write as jest.Mock).call(res, content); - await (res.end as jest.Mock).call(res); - - expect(next).toHaveBeenCalledWith(expect.any(Error)); - }); - }); -}); diff --git a/src/middleware/filter-middleware.ts b/src/middleware/filter-middleware.ts index 451cdd9..b63d4bd 100644 --- a/src/middleware/filter-middleware.ts +++ b/src/middleware/filter-middleware.ts @@ -7,26 +7,26 @@ import { MatchType } from '../types/filtering'; type WriteCallback = (error?: Error | null) => void; type EndCallback = () => void; -type WriteFunction = { - (chunk: any, callback?: WriteCallback): boolean; - (chunk: any, encoding: BufferEncoding, callback?: WriteCallback): boolean; +interface WriteFunction { + (chunk: unknown, callback?: WriteCallback): boolean; + (chunk: unknown, encoding: BufferEncoding, callback?: WriteCallback): boolean; } -type EndFunction = { +interface EndFunction { (cb?: EndCallback): void; - (chunk: any, cb?: EndCallback): void; - (chunk: any, encoding: BufferEncoding, cb?: EndCallback): void; + (chunk: unknown, cb?: EndCallback): void; + (chunk: unknown, encoding: BufferEncoding, cb?: EndCallback): void; } -type ResponseWrite = { - (chunk: any, callback?: WriteCallback): boolean; - (chunk: any, encoding: BufferEncoding, callback?: WriteCallback): boolean; +interface ResponseWrite { + (chunk: unknown, callback?: WriteCallback): boolean; + (chunk: unknown, encoding: BufferEncoding, callback?: WriteCallback): boolean; } -type ResponseEnd = { +interface ResponseEnd { (cb?: EndCallback): Response; - (chunk: any, cb?: EndCallback): Response; - (chunk: any, encoding: BufferEncoding, cb?: EndCallback): Response; + (chunk: unknown, cb?: EndCallback): Response; + (chunk: unknown, encoding: BufferEncoding, cb?: EndCallback): Response; } /** @@ -47,11 +47,19 @@ export class FilterMiddleware { let buffer = Buffer.from(''); // レスポンスデータを収集 - const newWrite: ResponseWrite = (chunk: any, encodingOrCallback?: BufferEncoding | WriteCallback, callback?: WriteCallback): boolean => { + const newWrite: ResponseWrite = (chunk: unknown, encodingOrCallback?: BufferEncoding | WriteCallback, callback?: WriteCallback): boolean => { if (chunk) { - const data = Buffer.isBuffer(chunk) - ? chunk - : Buffer.from(chunk); + let data: Buffer; + if (Buffer.isBuffer(chunk)) { + data = chunk; + } else if (typeof chunk === 'string') { + data = Buffer.from(chunk); + } else if (chunk instanceof Uint8Array) { + data = Buffer.from(chunk); + } else { + // その他の型の場合は文字列に変換してBufferを作成 + data = Buffer.from(String(chunk)); + } buffer = Buffer.concat([buffer, data]); } @@ -63,13 +71,20 @@ export class FilterMiddleware { res.write = newWrite; // レスポンスの最後でフィルタリングを適用 - const newEnd: ResponseEnd = (chunkOrCallback?: any, encodingOrCallback?: BufferEncoding | EndCallback, callback?: EndCallback): Response => { + const newEnd: ResponseEnd = (chunkOrCallback?: unknown, encodingOrCallback?: BufferEncoding | EndCallback, callback?: EndCallback): Response => { void (async () => { try { if (typeof chunkOrCallback !== 'function' && chunkOrCallback) { - const data = Buffer.isBuffer(chunkOrCallback) - ? chunkOrCallback - : Buffer.from(chunkOrCallback); + let data: Buffer; + if (Buffer.isBuffer(chunkOrCallback)) { + data = chunkOrCallback; + } else if (typeof chunkOrCallback === 'string') { + data = Buffer.from(chunkOrCallback); + } else if (chunkOrCallback instanceof Uint8Array) { + data = Buffer.from(chunkOrCallback); + } else { + data = Buffer.from(String(chunkOrCallback)); + } buffer = Buffer.concat([buffer, data]); } @@ -84,18 +99,18 @@ export class FilterMiddleware { // 更新されたコンテンツを送信 res.setHeader('content-length', buffer.length); if (typeof chunkOrCallback === 'function') { - originalEnd(buffer, chunkOrCallback); + originalEnd(buffer, chunkOrCallback as EndCallback); } else if (typeof encodingOrCallback === 'function') { - originalEnd(buffer, encodingOrCallback); + originalEnd(buffer, encodingOrCallback as EndCallback); } else { originalEnd(buffer, encodingOrCallback as BufferEncoding, callback); } } else { // 非HTMLコンテンツはそのまま送信 if (typeof chunkOrCallback === 'function') { - originalEnd(buffer, chunkOrCallback); + originalEnd(buffer, chunkOrCallback as EndCallback); } else if (typeof encodingOrCallback === 'function') { - originalEnd(buffer, encodingOrCallback); + originalEnd(buffer, encodingOrCallback as EndCallback); } else { originalEnd(buffer, encodingOrCallback as BufferEncoding, callback); } @@ -105,11 +120,14 @@ export class FilterMiddleware { 'フィルタリング処理中にエラーが発生しました', 'FILTER_ERROR', 500, - { error, url: req.url } + { + error: error instanceof Error ? error.message : String(error), + url: req.url + } ); next(appError); if (typeof chunkOrCallback === 'function') { - originalEnd(chunkOrCallback); + originalEnd(chunkOrCallback as EndCallback); } else { originalEnd(); } diff --git a/src/proxy/__tests__/error-handler.test.ts b/src/proxy/__tests__/error-handler.test.ts deleted file mode 100644 index 4015c47..0000000 --- a/src/proxy/__tests__/error-handler.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * エラーハンドリングミドルウェアのテスト - * - * @description - * エラーハンドリングミドルウェアの機能をテストします。 - * 各種エラーの処理が適切に行われることを確認します。 - */ - -import { Request, Response, NextFunction } from 'express'; -import { - AppError, - NetworkError, - LLMError, - ConfigError, - ProxyError, -} from '../../utils/errors'; -import { errorHandler } from '../error-handler'; -import { createLogger } from '../../utils/logger'; - -describe('エラーハンドリングミドルウェア', () => { - let mockRequest: Partial; - let mockResponse: Partial; - let nextFunction: NextFunction; - let mockJson: jest.Mock; - let mockStatus: jest.Mock; - let mockLogger: ReturnType; - - /** - * 各テストの前に実行されるセットアップ処理 - */ - beforeEach(() => { - // Expressのモックオブジェクトを設定 - mockJson = jest.fn(); - mockStatus = jest.fn().mockReturnValue({ json: mockJson }); - mockRequest = {}; - mockResponse = { - status: mockStatus, - }; - nextFunction = jest.fn(); - - // ロガーのモックを設定 - mockLogger = createLogger({ - port: 8080, - host: 'localhost', - ignoreRobotsTxt: false, - timeout: 30000, - userAgent: { - enabled: false, - rotate: false, - }, - llm: { - enabled: false, - type: 'ollama', - model: 'gemma', - }, - logging: { - level: 'error', - }, - filtering: { - enabled: false, - configPath: undefined, - }, - }); - }); - - /** - * AppErrorのハンドリングテスト - * - * @description - * AppErrorが正しく処理され、適切なステータスコードと - * エラーメッセージが返されることを確認します。 - */ - test('AppErrorを適切に処理する', () => { - // テスト用のエラーオブジェクトを作成 - const error = new AppError( - 'アプリケーションエラー', - 'APP_ERROR', - 500, - { detail: 'テスト' } - ); - - // エラーハンドラーを実行 - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - // ステータスコードとエラーレスポンスを検証 - expect(mockStatus).toHaveBeenCalledWith(500); - expect(mockJson).toHaveBeenCalledWith({ - error: { - message: 'アプリケーションエラー', - code: 'APP_ERROR', - metadata: { detail: 'テスト' }, - }, - }); - }); - - /** - * NetworkErrorのハンドリングテスト - * - * @description - * NetworkErrorが正しく処理され、503ステータスコードと - * 適切なエラーメッセージが返されることを確認します。 - */ - test('NetworkErrorを適切に処理する', () => { - // 元のエラーとNetworkErrorを作成 - const originalError = new Error('接続エラー'); - const error = new NetworkError('ネットワークエラー', originalError); - - // エラーハンドラーを実行 - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - // ステータスコードとエラーレスポンスを検証 - expect(mockStatus).toHaveBeenCalledWith(503); - expect(mockJson).toHaveBeenCalledWith({ - error: { - message: 'ネットワークエラー: ネットワークエラー', - code: 'NETWORK_ERROR', - }, - }); - }); - - /** - * LLMErrorのハンドリングテスト - * - * @description - * LLMErrorが正しく処理され、適切なステータスコードと - * メタデータを含むエラーメッセージが返されることを確認します。 - */ - test('LLMErrorを適切に処理する', () => { - // テスト用のLLMエラーを作成 - const error = new LLMError('LLMエラー', { model: 'test-model' }); - - // エラーハンドラーを実行 - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - // ステータスコードとエラーレスポンスを検証 - expect(mockStatus).toHaveBeenCalledWith(500); - expect(mockJson).toHaveBeenCalledWith({ - error: { - message: 'LLMエラー: LLMエラー', - code: 'LLM_ERROR', - metadata: { model: 'test-model' }, - }, - }); - }); - - /** - * ConfigErrorのハンドリングテスト - * - * @description - * ConfigErrorが正しく処理され、適切なステータスコードと - * 設定関連のメタデータを含むエラーメッセージが返されることを - * 確認します。 - */ - test('ConfigErrorを適切に処理する', () => { - // テスト用の設定エラーを作成 - const error = new ConfigError('設定エラー', { config: 'test' }); - - // エラーハンドラーを実行 - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - // ステータスコードとエラーレスポンスを検証 - expect(mockStatus).toHaveBeenCalledWith(500); - expect(mockJson).toHaveBeenCalledWith({ - error: { - message: '設定エラー: 設定エラー', - code: 'CONFIG_ERROR', - metadata: { config: 'test' }, - }, - }); - }); - - /** - * ProxyErrorのハンドリングテスト - * - * @description - * ProxyErrorが正しく処理され、502ステータスコードと - * URL情報を含むメタデータが返されることを確認します。 - */ - test('ProxyErrorを適切に処理する', () => { - // テスト用のプロキシエラーを作成 - const error = new ProxyError( - 'プロキシエラー', - { url: 'http://example.com' } - ); - - // エラーハンドラーを実行 - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - // ステータスコードとエラーレスポンスを検証 - expect(mockStatus).toHaveBeenCalledWith(502); - expect(mockJson).toHaveBeenCalledWith({ - error: { - message: 'プロキシエラー: プロキシエラー', - code: 'PROXY_ERROR', - metadata: { url: 'http://example.com' }, - }, - }); - }); -}); - - /** - * 一般的なErrorのハンドリングテスト - * - * @description - * 標準のErrorオブジェクトが正しく処理され、500ステータスコードと - * 適切なエラーメッセージが返されることを確認します。 - */ - test('一般的なErrorを適切に処理する', () => { - // テスト用の標準エラーを作成 - const error = new Error('一般的なエラー'); - - // エラーハンドラーを実行 - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - // ステータスコードとエラーレスポンスを検証 - expect(mockStatus).toHaveBeenCalledWith(500); - expect(mockJson).toHaveBeenCalledWith({ - error: { - message: '内部サーバーエラーが発生しました', - code: 'INTERNAL_SERVER_ERROR', - }, - }); - }); - - test('開発環境でのスタックトレース表示', () => { - const originalNodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'development'; - - const error = new Error('開発環境エラー'); - error.stack = 'Error: 開発環境エラー\n at Test'; - errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction); - - expect(mockJson).toHaveBeenCalledWith({ - error: { - message: '内部サーバーエラーが発生しました', - code: 'INTERNAL_SERVER_ERROR', - stack: 'Error: 開発環境エラー\n at Test', - }, - }); - - process.env.NODE_ENV = originalNodeEnv; - }); - diff --git a/src/proxy/__tests__/server.test.ts b/src/proxy/__tests__/server.test.ts deleted file mode 100644 index 6a2ea64..0000000 --- a/src/proxy/__tests__/server.test.ts +++ /dev/null @@ -1,611 +0,0 @@ -import { ProxyServer } from '../server'; -import { Config } from '../../config'; -import { createLLMFilter } from '../../llm'; -import express from 'express'; -import httpProxy from 'http-proxy'; - -// モックの設定 -jest.mock('express'); -jest.mock('http-proxy'); -jest.mock('../../llm'); -jest.mock('../../utils/logger', () => ({ - createLogger: jest.fn().mockReturnValue({ - info: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - }), -})); - -describe('プロキシサーバー', () => { - let config: Config; - let mockApp: any; - let mockProxy: any; - let mockFilter: any; - - beforeEach(() => { - // モックのリセット - jest.clearAllMocks(); - - // 設定オブジェクトの作成 - config = { - port: 8080, - host: 'localhost', - ignoreRobotsTxt: false, - llm: { - type: 'ollama', - enabled: true, - model: 'gemma', - baseUrl: 'http://localhost:11434', - }, - logging: { - level: 'info', - }, - userAgent: { - enabled: false, - rotate: false, - }, - } as Config; - - // モックの作成 - mockApp = { - use: jest.fn(), - listen: jest.fn().mockImplementation((port, host, callback) => { - if (callback) callback(); - return mockApp; - }), - on: jest.fn().mockReturnThis(), - }; - - // EventEmitterを継承したモックプロキシを作成 - const { EventEmitter } = require('events'); - mockProxy = new EventEmitter(); - mockProxy.web = jest.fn(); - mockProxy.on = jest.fn().mockImplementation((event, callback) => { - EventEmitter.prototype.on.call(mockProxy, event, callback); - return mockProxy; - }); - - mockFilter = { - name: 'TestFilter', - filter: jest - .fn() - .mockImplementation((content) => - Promise.resolve('フィルタリングされたコンテンツ'), - ), - }; - - // モックの設定 - (express as unknown as jest.Mock).mockReturnValue(mockApp); - (httpProxy.createProxyServer as jest.Mock).mockReturnValue(mockProxy); - (createLLMFilter as jest.Mock).mockReturnValue(mockFilter); - - // モックプロキシのイベントリスナーをクリア - mockProxy.removeAllListeners(); - }); - - test('プロキシサーバーが正しく作成される', () => { - const server = new ProxyServer(config); - - // Expressアプリが作成されたか確認 - expect(express).toHaveBeenCalled(); - - // プロキシが作成されたか確認 - expect(httpProxy.createProxyServer).toHaveBeenCalled(); - }); - - test('プロキシサーバーがリクエストを正しく処理する', () => { - // リクエストとレスポンスをモック - const req = { url: 'http://example.com' }; - const res = { status: jest.fn().mockReturnThis(), end: jest.fn() }; - const next = jest.fn(); - - // サーバーを作成 - const server = new ProxyServer(config); - - // app.useが呼ばれたか確認 - expect(mockApp.use).toHaveBeenCalled(); - - // プロキシリクエストをシミュレート - // 最後のミドルウェアのコールバックを取得 - const lastCall = mockApp.use.mock.calls[mockApp.use.mock.calls.length - 1]; - const path = lastCall[0]; - const middleware = lastCall[1]; - - // ミドルウェアを実行 - middleware(req, res, next); - - // proxy.webが正しく呼ばれたか確認 - expect(mockProxy.web).toHaveBeenCalledWith(req, res, { - target: req.url, - changeOrigin: true, - timeout: 30000, - }); - }); - - test('proxyReqイベントが正しく処理される', () => { - // ignoreRobotsTxtをtrueに設定 - config.ignoreRobotsTxt = true; - - const server = new ProxyServer(config); - - // proxyReqイベントハンドラを取得 - const proxyReqHandler = mockProxy.on.mock.calls.find( - (call: any[]) => call[0] === 'proxyReq', - )[1]; - - // リクエストをモック - const proxyReq = { setHeader: jest.fn() }; - const req = {}; - const res = {}; - - // ハンドラを実行 - proxyReqHandler(proxyReq, req, res); - - // User-Agentヘッダーが設定されたか確認 - expect(proxyReq.setHeader).toHaveBeenCalledWith( - 'User-Agent', - 'SubtractProxy/1.0', - ); - }); - - test('proxyResイベントの登録が正しく行われる', () => { - const server = new ProxyServer(config); - - // proxyResイベントが登録されたか確認 - expect(mockProxy.on).toHaveBeenCalledWith('proxyRes', expect.any(Function)); - }); - - test('フィルターの追加が正しく動作する', () => { - const server = new ProxyServer(config); - - // フィルターを追加 - server.addFilter(mockFilter); - - // フィルターが追加されたか確認する方法がないので、 - // フィルターの動作をテストする - - // proxyResイベントハンドラを取得 - const proxyResHandler = mockProxy.on.mock.calls.find( - (call: any[]) => call[0] === 'proxyRes', - )[1]; - - // レスポンスをモック - const proxyRes = { - on: jest.fn().mockImplementation((event, callback) => { - if (event === 'data') { - callback('test content'); - } - if (event === 'end') { - callback(); - } - return proxyRes; - }), - }; - - const req = {}; - const res = { end: jest.fn() }; - - // ハンドラを実行 - proxyResHandler(proxyRes, req, res); - - // フィルターが呼ばれたか確認 - expect(mockFilter.filter).toHaveBeenCalled(); - }); - - test('サーバーが正しく起動する', () => { - const server = new ProxyServer(config); - - // process.onをモック - const originalProcessOn = process.on; - process.on = jest.fn().mockReturnThis() as any; - - // サーバーを起動 - server.start(); - - // listenが正しく呼ばれたか確認 - expect(mockApp.listen).toHaveBeenCalledWith( - config.port, - config.host, - expect.any(Function), - ); - - // プロセスイベントハンドラーが登録されたか確認 - expect(process.on).toHaveBeenCalledWith( - 'uncaughtException', - expect.any(Function), - ); - expect(process.on).toHaveBeenCalledWith( - 'unhandledRejection', - expect.any(Function), - ); - expect(process.on).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); - expect(process.on).toHaveBeenCalledWith('SIGINT', expect.any(Function)); - - // モックを元に戻す - process.on = originalProcessOn; - }); - - test('プロキシイベントが正しく設定される', () => { - const server = new ProxyServer(config); - - // proxyResイベントが設定されたか確認 - expect(mockProxy.on).toHaveBeenCalledWith('proxyRes', expect.any(Function)); - - // proxyReqイベントが設定されたか確認 - expect(mockProxy.on).toHaveBeenCalledWith('proxyReq', expect.any(Function)); - }); - - test('フィルターが正しく追加される', () => { - const server = new ProxyServer(config); - server.addFilter(mockFilter); - - // サーバーが起動するとリッスンする - server.start(); - expect(mockApp.listen).toHaveBeenCalledWith( - config.port, - config.host, - expect.any(Function), - ); - }); - - test('プロキシエラーが正しく処理される', () => { - const server = new ProxyServer(config); - - // プロキシのエラーハンドラーを取得 - const errorHandler = mockProxy.on.mock.calls.find( - (call: any[]) => call[0] === 'error', - )[1]; - - // リクエストとレスポンスをモック - const req = {}; - const res = { - headersSent: false, - writeHead: jest.fn(), - end: jest.fn(), - }; - - // エラーハンドラーを実行 - errorHandler(new Error('プロキシエラー'), req, res); - - // レスポンスが正しく設定されたか確認 - expect(res.writeHead).toHaveBeenCalledWith(500, { - 'Content-Type': 'application/json', - }); - expect(res.end).toHaveBeenCalled(); - }); - - test('サーバーが正しく停止される', () => { - // サーバーを作成 - const server = new ProxyServer(config); - - // サーバーを起動 - server.start(); - - // サーバーの停止をモック - mockApp.close = jest.fn().mockImplementation((callback) => { - if (callback) callback(); - }); - - // サーバーを停止 - server.stop(); - }); - - test('favicon.icoリクエストが正しく処理される', () => { - // リクエストとレスポンスをモック - const req = { url: '/favicon.ico' }; - const res = { status: jest.fn().mockReturnThis(), end: jest.fn() }; - const next = jest.fn(); - - // サーバーを作成 - const server = new ProxyServer(config); - - // 最後のミドルウェアを取得して実行 - const lastCall = mockApp.use.mock.calls[mockApp.use.mock.calls.length - 1]; - const middleware = lastCall[1]; - - // ミドルウェアを実行 - middleware(req, res, next); - - // statusとendが正しく呼ばれたか確認 - expect(res.status).toHaveBeenCalledWith(204); - expect(res.end).toHaveBeenCalled(); - // proxy.webは呼ばれないことを確認 - expect(mockProxy.web).not.toHaveBeenCalled(); - }); - - test('ミドルウェアのエラーハンドリングが正しく動作する', () => { - // リクエストとレスポンスをモック - const req = { url: 'http://example.com' }; - const res = {}; - const next = jest.fn(); - - // サーバーを作成 - const server = new ProxyServer(config); - - // 最後のミドルウェアを取得 - const lastCall = mockApp.use.mock.calls[mockApp.use.mock.calls.length - 1]; - const middleware = lastCall[1]; - - // proxy.webがエラーを投げるように設定 - mockProxy.web.mockImplementationOnce(() => { - throw new Error('プロキシエラー'); - }); - - // ミドルウェアを実行 - middleware(req, res, next); - - // nextがエラーとともに呼ばれたか確認 - expect(next).toHaveBeenCalledWith(expect.any(Error)); - }); - - describe('User-Agent機能', () => { - test('User-Agentが無効の場合、ヘッダーが設定されない', () => { - config.userAgent.enabled = false; - const server = new ProxyServer(config); - server.start(); - - // プロキシリクエストのイベントをシミュレート - const mockProxyReq = { - setHeader: jest.fn(), - }; - const mockReq = { - url: 'http://example.com', - method: 'GET', - }; - - // proxyReqイベントを発火 - mockProxy.emit('proxyReq', mockProxyReq, mockReq, {}); - - // User-Agentヘッダーが設定されていないことを確認 - expect(mockProxyReq.setHeader).not.toHaveBeenCalledWith('User-Agent', expect.any(String)); - }); - - test('カスタムUser-Agentが正しく設定される', () => { - const customUA = 'CustomUserAgent/1.0'; - config.userAgent = { - enabled: true, - rotate: false, - value: customUA, - }; - const server = new ProxyServer(config); - server.start(); - - const mockProxyReq = { - setHeader: jest.fn(), - }; - const mockReq = { - url: 'http://example.com', - method: 'GET', - }; - - mockProxy.emit('proxyReq', mockProxyReq, mockReq, {}); - - expect(mockProxyReq.setHeader).toHaveBeenCalledWith('User-Agent', customUA); - }); - - test('ローテーションが有効な場合、User-Agentが切り替わる', () => { - const presets = ['UA1', 'UA2', 'UA3']; - config.userAgent = { - enabled: true, - rotate: true, - presets, - }; - const server = new ProxyServer(config); - server.start(); - - const mockProxyReq1 = { setHeader: jest.fn() }; - const mockProxyReq2 = { setHeader: jest.fn() }; - const mockReq = { - url: 'http://example.com', - method: 'GET', - }; - - // 複数回リクエストをシミュレート - mockProxy.emit('proxyReq', mockProxyReq1, mockReq, {}); - mockProxy.emit('proxyReq', mockProxyReq2, mockReq, {}); - - // 異なるUser-Agentが使用されていることを確認 - const ua1 = mockProxyReq1.setHeader.mock.calls[0][1]; - const ua2 = mockProxyReq2.setHeader.mock.calls[0][1]; - expect(presets).toContain(ua1); - expect(presets).toContain(ua2); - expect(ua1).not.toBe(ua2); - }); - }); - - test('サーバーのエラーハンドリングが正しく動作する', () => { - const server = new ProxyServer(config); - - // サーバーを起動 - server.start(); - - // サーバーのエラーハンドラーをシミュレート - const serverErrorHandler = mockApp.on.mock.calls.find( - (call: any[]) => call[0] === 'error', - )?.[1]; - - // EADDRINUSEエラーをシミュレート - if (serverErrorHandler) { - const error = new Error('ポートが使用中です') as NodeJS.ErrnoException; - error.code = 'EADDRINUSE'; - serverErrorHandler(error); - } - - // その他のエラーをシミュレート - if (serverErrorHandler) { - const error = new Error('その他のサーバーエラー'); - serverErrorHandler(error); - } - }); - - test('シャットダウンシグナルが正しく処理される', () => { - // オリジナルのprocess.exitを保存 - const originalProcessExit = process.exit; - process.exit = jest.fn() as any; - - // オリジナルのprocess.onを保存 - const originalProcessOn = process.on; - const processOnMock = jest.fn().mockReturnThis(); - process.on = processOnMock as any; - - // オリジナルのsetTimeoutを保存 - const originalSetTimeout = global.setTimeout; - (global.setTimeout as any) = jest - .fn() - .mockImplementation((callback, ms) => { - return { unref: jest.fn() }; - }); - - // サーバーを作成して起動 - const server = new ProxyServer(config); - server.start(); - - // SIGTERMハンドラーを取得 - const sigtermHandler = processOnMock.mock.calls.find( - (call: any[]) => call[0] === 'SIGTERM', - )?.[1]; - - // サーバーの停止をモック - mockApp.close = jest.fn().mockImplementation((callback) => { - if (callback) callback(); - }); - - // SIGTERMハンドラーを実行 - if (sigtermHandler) { - sigtermHandler(); - } - - // process.exitが呼ばれたか確認 - expect(process.exit).toHaveBeenCalledWith(0); - - // モックを元に戻す - process.exit = originalProcessExit; - process.on = originalProcessOn; - global.setTimeout = originalSetTimeout; - }); - - test('シャットダウンタイムアウトが正しく処理される', () => { - // オリジナルのprocess.exitを保存 - const originalProcessExit = process.exit; - process.exit = jest.fn() as any; - - // オリジナルのprocess.onを保存 - const originalProcessOn = process.on; - const processOnMock = jest.fn().mockReturnThis(); - process.on = processOnMock as any; - - // オリジナルのsetTimeoutを保存 - const originalSetTimeout = global.setTimeout; - let timeoutCallback: Function | null = null; - (global.setTimeout as any) = jest - .fn() - .mockImplementation((callback, ms) => { - timeoutCallback = callback as Function; - return { unref: jest.fn() }; - }); - - // サーバーを作成して起動 - const server = new ProxyServer(config); - server.start(); - - // SIGTERMハンドラーを取得 - const sigtermHandler = processOnMock.mock.calls.find( - (call: any[]) => call[0] === 'SIGTERM', - )?.[1]; - - // サーバーの停止をモックし、コールバックを呼ばないように設定 - mockApp.close = jest.fn(); - - // SIGTERMハンドラーを実行 - if (sigtermHandler) { - sigtermHandler(); - } - - // タイムアウトコールバックを実行 - if (timeoutCallback) { - (timeoutCallback as Function)(); - } - - // タイムアウト後のprocess.exitが呼ばれたか確認 - expect(process.exit).toHaveBeenCalledWith(1); - - // モックを元に戻す - process.exit = originalProcessExit; - process.on = originalProcessOn; - global.setTimeout = originalSetTimeout; - }); - - test('未処理例外ハンドラーが正しく動作する', () => { - // オリジナルのprocess.exitを保存 - const originalProcessExit = process.exit; - process.exit = jest.fn() as any; - - // オリジナルのprocess.onを保存 - const originalProcessOn = process.on; - const processOnMock = jest.fn().mockReturnThis(); - process.on = processOnMock as any; - - // オリジナルのsetTimeoutを保存 - const originalSetTimeout = global.setTimeout; - let timeoutCallback: Function | null = null; - (global.setTimeout as any) = jest - .fn() - .mockImplementation((callback, ms) => { - timeoutCallback = callback as Function; - return { unref: jest.fn() }; - }); - - // サーバーを作成して起動 - const server = new ProxyServer(config); - server.start(); - - // uncaughtExceptionハンドラーを取得 - const uncaughtExceptionHandler = processOnMock.mock.calls.find( - (call: any[]) => call[0] === 'uncaughtException', - )?.[1]; - - // ハンドラーを実行 - if (uncaughtExceptionHandler) { - uncaughtExceptionHandler(new Error('未処理例外')); - } - - // タイムアウトコールバックを実行 - if (timeoutCallback) { - (timeoutCallback as Function)(); - } - - // process.exitが呼ばれたか確認 - expect(process.exit).toHaveBeenCalledWith(1); - - // モックを元に戻す - process.exit = originalProcessExit; - process.on = originalProcessOn; - global.setTimeout = originalSetTimeout; - }); - - test('未処理Promiseリジェクションハンドラーが正しく動作する', () => { - // オリジナルのprocess.onを保存 - const originalProcessOn = process.on; - const processOnMock = jest.fn().mockReturnThis(); - process.on = processOnMock as any; - - // サーバーを作成して起動 - const server = new ProxyServer(config); - server.start(); - - // unhandledRejectionハンドラーを取得 - const unhandledRejectionHandler = processOnMock.mock.calls.find( - (call: any[]) => call[0] === 'unhandledRejection', - )?.[1]; - - // ハンドラーを実行 - if (unhandledRejectionHandler) { - unhandledRejectionHandler(new Error('未処理リジェクション')); - unhandledRejectionHandler('文字列リジェクション'); - } - - // モックを元に戻す - process.on = originalProcessOn; - }); -}); diff --git a/src/services/__tests__/filter-service.test.ts b/src/services/__tests__/filter-service.test.ts deleted file mode 100644 index 2fe85e6..0000000 --- a/src/services/__tests__/filter-service.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { FilterService } from '../filter-service'; -import { FilterRule, MatchType, RulePriority } from '../../types/filtering'; -import { AppError } from '../../utils/errors'; - -describe('FilterService', () => { - let filterService: FilterService; - - beforeEach(() => { - filterService = new FilterService(); - }); - - describe('ルールの追加', () => { - it('有効なルールを追加できること', () => { - const rule = { - name: 'テストルール', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.MEDIUM, - enabled: true, - }; - - const addedRule = filterService.addRule(rule); - - expect(addedRule.id).toBeDefined(); - expect(addedRule.name).toBe(rule.name); - expect(addedRule.pattern).toBe(rule.pattern); - expect(addedRule.createdAt).toBeInstanceOf(Date); - expect(addedRule.updatedAt).toBeInstanceOf(Date); - }); - - it('無効なルール名でエラーとなること', () => { - const rule = { - name: '', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.MEDIUM, - enabled: true, - }; - - expect(() => filterService.addRule(rule)).toThrow(AppError); - }); - - it('無効な正規表現パターンでエラーとなること', () => { - const rule = { - name: 'テストルール', - matchType: MatchType.REGEX, - pattern: '[', // 無効な正規表現 - priority: RulePriority.MEDIUM, - enabled: true, - }; - - expect(() => filterService.addRule(rule)).toThrow(AppError); - }); - }); - - describe('ルールの更新', () => { - let existingRule: FilterRule; - - beforeEach(() => { - existingRule = filterService.addRule({ - name: '既存ルール', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.MEDIUM, - enabled: true, - }); - }); - - it('ルールを正しく更新できること', () => { - const updates = { - name: '更新後のルール', - pattern: '[a-z]+', - }; - - const updatedRule = filterService.updateRule(existingRule.id, updates); - - expect(updatedRule.name).toBe(updates.name); - expect(updatedRule.pattern).toBe(updates.pattern); - expect(updatedRule.updatedAt).not.toBe(existingRule.updatedAt); - }); - - it('存在しないルールIDでエラーとなること', () => { - expect(() => { - filterService.updateRule('non-existent-id', { name: '新しい名前' }); - }).toThrow(AppError); - }); - }); - - describe('ルールの削除', () => { - let existingRule: FilterRule; - - beforeEach(() => { - existingRule = filterService.addRule({ - name: '削除対象ルール', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.MEDIUM, - enabled: true, - }); - }); - - it('ルールを正しく削除できること', () => { - filterService.deleteRule(existingRule.id); - expect(() => { - filterService.updateRule(existingRule.id, {}); - }).toThrow(AppError); - }); - - it('存在しないルールIDでエラーとなること', () => { - expect(() => { - filterService.deleteRule('non-existent-id'); - }).toThrow(AppError); - }); - }); - - describe('フィルタリングの適用', () => { - it('正規表現ルールが正しく適用されること', async () => { - filterService.addRule({ - name: '数字除去', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.MEDIUM, - enabled: true, - }); - - const result = await filterService.applyFilters('テスト123テスト456'); - expect(result).toBe('テストテスト'); - }); - - it('無効化されたルールが適用されないこと', async () => { - filterService.addRule({ - name: '数字除去', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.MEDIUM, - enabled: false, - }); - - const result = await filterService.applyFilters('テスト123テスト456'); - expect(result).toBe('テスト123テスト456'); - }); - - it('優先度順にルールが適用されること', async () => { - filterService.addRule({ - name: '数字除去', - matchType: MatchType.REGEX, - pattern: '\\d+', - priority: RulePriority.LOW, - enabled: true, - }); - - filterService.addRule({ - name: 'テスト文字除去', - matchType: MatchType.REGEX, - pattern: 'テスト', - priority: RulePriority.HIGH, - enabled: true, - }); - - const result = await filterService.applyFilters('テスト123テスト456'); - expect(result).toBe('123456'); - }); - }); -}); diff --git a/src/utils/__tests__/env.test.ts b/src/utils/__tests__/env.test.ts deleted file mode 100644 index ec0c7f3..0000000 --- a/src/utils/__tests__/env.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getEnv, getEnvNumber, getEnvBoolean } from '../env'; - -describe('環境変数ユーティリティのテスト', () => { - // テスト前の準備 - const originalEnv = process.env; - - beforeEach(() => { - // 環境変数をリセット - process.env = { ...originalEnv }; - }); - - afterAll(() => { - // テスト後に環境変数を元に戻す - process.env = originalEnv; - }); - - describe('getEnv', () => { - test('環境変数が存在する場合、その値を返す', () => { - process.env.TEST_VAR = 'test-value'; - expect(getEnv('TEST_VAR')).toBe('test-value'); - }); - - test('環境変数が存在しない場合、デフォルト値を返す', () => { - expect(getEnv('NON_EXISTENT_VAR', 'default')).toBe('default'); - }); - - test('環境変数が存在せずデフォルト値も指定されていない場合、undefinedを返す', () => { - expect(getEnv('NON_EXISTENT_VAR')).toBeUndefined(); - }); - }); - - describe('getEnvNumber', () => { - test('環境変数が数値として解析可能な場合、数値を返す', () => { - process.env.TEST_NUM = '123'; - expect(getEnvNumber('TEST_NUM')).toBe(123); - }); - - test('環境変数が数値として解析不可能な場合、デフォルト値を返す', () => { - process.env.TEST_NUM = 'not-a-number'; - expect(getEnvNumber('TEST_NUM', 456)).toBe(456); - }); - - test('環境変数が存在しない場合、デフォルト値を返す', () => { - expect(getEnvNumber('NON_EXISTENT_NUM', 789)).toBe(789); - }); - - test('環境変数が存在せずデフォルト値も指定されていない場合、undefinedを返す', () => { - expect(getEnvNumber('NON_EXISTENT_NUM')).toBeUndefined(); - }); - }); - - describe('getEnvBoolean', () => { - test('環境変数が"true"の場合、trueを返す', () => { - process.env.TEST_BOOL = 'true'; - expect(getEnvBoolean('TEST_BOOL')).toBe(true); - }); - - test('環境変数が"TRUE"(大文字)の場合、trueを返す', () => { - process.env.TEST_BOOL = 'TRUE'; - expect(getEnvBoolean('TEST_BOOL')).toBe(true); - }); - - test('環境変数が"true"以外の場合、falseを返す', () => { - process.env.TEST_BOOL = 'false'; - expect(getEnvBoolean('TEST_BOOL')).toBe(false); - - process.env.TEST_BOOL = 'anything'; - expect(getEnvBoolean('TEST_BOOL')).toBe(false); - }); - - test('環境変数が存在しない場合、デフォルト値を返す', () => { - expect(getEnvBoolean('NON_EXISTENT_BOOL', true)).toBe(true); - expect(getEnvBoolean('NON_EXISTENT_BOOL', false)).toBe(false); - }); - - test('環境変数が存在せずデフォルト値も指定されていない場合、undefinedを返す', () => { - expect(getEnvBoolean('NON_EXISTENT_BOOL')).toBeUndefined(); - }); - }); -}); diff --git a/src/utils/__tests__/errors.test.ts b/src/utils/__tests__/errors.test.ts deleted file mode 100644 index 18b4bfe..0000000 --- a/src/utils/__tests__/errors.test.ts +++ /dev/null @@ -1,308 +0,0 @@ -/** - * エラーハンドリングのテスト - */ - -import winston from 'winston'; -import { - AppError, - NetworkError, - LLMError, - ConfigError, - ProxyError, - isAppError, - logError, - handleErrorWithFallback, -} from '../errors'; - -// モックロガー -const mockLogger = { - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), - silly: jest.fn(), - verbose: jest.fn(), - http: jest.fn(), - log: jest.fn(), - add: jest.fn(), - remove: jest.fn(), - clear: jest.fn(), - close: jest.fn(), - profile: jest.fn(), - startTimer: jest.fn(), - exceptions: { - handle: jest.fn(), - unhandle: jest.fn(), - logger: undefined, - handlers: [], - catcher: jest.fn(), - getAllInfo: jest.fn(), - getProcessInfo: jest.fn(), - getOsInfo: jest.fn(), - getTrace: jest.fn() - }, - rejections: { - handle: jest.fn(), - unhandle: jest.fn(), - logger: undefined, - handlers: [], - catcher: jest.fn(), - getAllInfo: jest.fn(), - getProcessInfo: jest.fn(), - getOsInfo: jest.fn(), - getTrace: jest.fn() - }, - profilers: new Map(), - exitOnError: false, - silent: false, - format: winston.format.json(), - levels: winston.config.npm.levels, - level: 'info', - transports: [], - write: jest.fn(), - stream: jest.fn(), - setMaxListeners: jest.fn(), - getMaxListeners: jest.fn(), - emit: jest.fn(), - addListener: jest.fn(), - on: jest.fn(), - once: jest.fn(), - prependListener: jest.fn(), - prependOnceListener: jest.fn(), - removeListener: jest.fn(), - off: jest.fn(), - removeAllListeners: jest.fn(), - listeners: jest.fn(), - rawListeners: jest.fn(), - listenerCount: jest.fn(), - eventNames: jest.fn(), - help: jest.fn(), - data: jest.fn(), - prompt: jest.fn(), - input: jest.fn(), - emerg: jest.fn(), - alert: jest.fn(), - crit: jest.fn(), - warning: jest.fn(), - notice: jest.fn(), - eror: jest.fn(), - child: jest.fn(), - defaultMeta: {}, - isLevelEnabled: jest.fn(), - query: jest.fn(), - configure: jest.fn(), - cli: jest.fn(), - handleRejections: jest.fn(), - handleExceptions: jest.fn(), - unhandleRejections: jest.fn(), - unhandleExceptions: jest.fn(), - createLogger: jest.fn(), - loggers: new Map(), - container: jest.fn(), - addColors: jest.fn(), - setLevels: jest.fn(), - config: jest.fn(), - addLevel: jest.fn(), - clone: jest.fn(), - pause: jest.fn(), - resume: jest.fn(), - destroy: jest.fn(), - _destroy: jest.fn(), - _final: jest.fn(), - _write: jest.fn(), - _writev: jest.fn(), - _read: jest.fn(), - pipe: jest.fn(), - unpipe: jest.fn(), - unshift: jest.fn(), - wrap: jest.fn(), - push: jest.fn(), - _transform: jest.fn(), - _flush: jest.fn(), - cork: jest.fn(), - uncork: jest.fn(), - setEncoding: jest.fn(), - read: jest.fn(), - isPaused: jest.fn(), - setDefaultEncoding: jest.fn() -} as unknown as winston.Logger; - -describe('エラークラスのテスト', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('AppError', () => { - test('基本的なエラー情報を持つ', () => { - const error = new AppError('テストエラー', 'TEST_ERROR'); - expect(error).toBeInstanceOf(Error); - expect(error.message).toBe('テストエラー'); - expect(error.code).toBe('TEST_ERROR'); - expect(error.statusCode).toBe(500); - expect(error.isOperational).toBe(true); - }); - - test('メタデータを設定できる', () => { - const metadata = { detail: 'テスト詳細' }; - const error = new AppError('テストエラー', 'TEST_ERROR', 400, metadata); - expect(error.metadata).toEqual(metadata); - expect(error.statusCode).toBe(400); - }); - - test('運用エラーフラグを設定できる', () => { - const error = new AppError('システムエラー', 'SYSTEM_ERROR', 500, undefined, false); - expect(error.isOperational).toBe(false); - }); - - test('スタックトレースを設定できる', () => { - const customStack = 'カスタムスタック'; - const error = new AppError('テストエラー', 'TEST_ERROR', 500, undefined, true, customStack); - expect(error.stack).toBe(customStack); - }); - }); - - describe('NetworkError', () => { - test('ネットワークエラーを生成する', () => { - const originalError = new Error('元のエラー'); - const error = new NetworkError('接続エラー', originalError); - expect(error).toBeInstanceOf(AppError); - expect(error.message).toBe('ネットワークエラー: 接続エラー'); - expect(error.code).toBe('NETWORK_ERROR'); - expect(error.statusCode).toBe(503); - expect(error.cause).toBe(originalError); - }); - }); - - describe('LLMError', () => { - test('LLMエラーを生成する', () => { - const metadata = { model: 'test-model' }; - const error = new LLMError('モデルエラー', metadata); - expect(error).toBeInstanceOf(AppError); - expect(error.message).toBe('LLMエラー: モデルエラー'); - expect(error.code).toBe('LLM_ERROR'); - expect(error.statusCode).toBe(500); - expect(error.metadata).toEqual(metadata); - }); - }); - - describe('ConfigError', () => { - test('設定エラーを生成する', () => { - const metadata = { config: 'test.json' }; - const error = new ConfigError('設定ファイルが見つかりません', metadata); - expect(error).toBeInstanceOf(AppError); - expect(error.message).toBe('設定エラー: 設定ファイルが見つかりません'); - expect(error.code).toBe('CONFIG_ERROR'); - expect(error.statusCode).toBe(500); - expect(error.metadata).toEqual(metadata); - }); - }); - - describe('ProxyError', () => { - test('プロキシエラーを生成する', () => { - const metadata = { url: 'http://example.com' }; - const error = new ProxyError('プロキシ接続エラー', metadata); - expect(error).toBeInstanceOf(AppError); - expect(error.message).toBe('プロキシエラー: プロキシ接続エラー'); - expect(error.code).toBe('PROXY_ERROR'); - expect(error.statusCode).toBe(502); - expect(error.metadata).toEqual(metadata); - }); - }); - - describe('isAppError', () => { - test('AppErrorのインスタンスを正しく判定する', () => { - const appError = new AppError('テストエラー'); - const networkError = new NetworkError('ネットワークエラー'); - const standardError = new Error('標準エラー'); - - expect(isAppError(appError)).toBe(true); - expect(isAppError(networkError)).toBe(true); - expect(isAppError(standardError)).toBe(false); - expect(isAppError(null)).toBe(false); - expect(isAppError(undefined)).toBe(false); - expect(isAppError({})).toBe(false); - }); - }); -}); - -describe('エラーユーティリティのテスト', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('logError', () => { - test('AppErrorの詳細情報がログに記録される', () => { - const error = new AppError('テストエラー', 'TEST_ERROR', 400, { test: true }); - logError(mockLogger, error, 'テスト'); - expect(mockLogger.error).toHaveBeenCalledWith('エラーが発生しました:', expect.objectContaining({ - message: 'テストエラー', - code: 'TEST_ERROR', - statusCode: 400, - metadata: { test: true }, - context: 'テスト', - })); - }); - - test('標準エラーが適切にログに記録される', () => { - const error = new Error('標準エラー'); - logError(mockLogger, error, 'テスト'); - expect(mockLogger.error).toHaveBeenCalledWith('エラーが発生しました:', expect.objectContaining({ - message: '標準エラー', - stack: expect.any(String), - context: 'テスト' - })); - }); - }); - - describe('handleErrorWithFallback', () => { - test('操作が成功した場合は結果を返す', async () => { - const operation = jest.fn().mockResolvedValue('成功'); - const fallback = jest.fn().mockReturnValue('フォールバック'); - - const result = await handleErrorWithFallback( - mockLogger, - operation, - fallback - ); - - expect(result).toBe('成功'); - expect(operation).toHaveBeenCalled(); - expect(fallback).not.toHaveBeenCalled(); - expect(mockLogger.error).not.toHaveBeenCalled(); - }); - - test('操作が失敗した場合はフォールバック値を返す', async () => { - const error = new AppError('テストエラー'); - const operation = jest.fn().mockRejectedValue(error); - const fallback = jest.fn().mockResolvedValue('フォールバック'); - - const result = await handleErrorWithFallback( - mockLogger, - operation, - fallback - ); - - expect(result).toBe('フォールバック'); - expect(operation).toHaveBeenCalled(); - expect(fallback).toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalled(); - }); - - test('フォールバック処理も失敗した場合のエラーハンドリング', async () => { - const error = new AppError('テストエラー'); - const fallbackError = new Error('フォールバックエラー'); - const operation = jest.fn().mockRejectedValue(error); - const fallback = jest.fn().mockRejectedValue(fallbackError); - - await expect(handleErrorWithFallback( - mockLogger, - operation, - fallback - )).rejects.toThrow('フォールバックエラー'); - expect(operation).toHaveBeenCalled(); - expect(fallback).toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalled(); - }); - }); -}); - diff --git a/src/utils/__tests__/file.test.ts b/src/utils/__tests__/file.test.ts deleted file mode 100644 index 80377b1..0000000 --- a/src/utils/__tests__/file.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileExists, readJsonFile, resolveProjectPath } from '../file'; - -// fsモジュールをモック -jest.mock('fs'); - -describe('ファイルユーティリティのテスト', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - describe('fileExists', () => { - test('ファイルが存在する場合、trueを返す', () => { - jest.spyOn(fs, 'existsSync').mockReturnValue(true); - - const result = fileExists('/path/to/file.json'); - expect(result).toBe(true); - expect(fs.existsSync).toHaveBeenCalledWith('/path/to/file.json'); - }); - - test('ファイルが存在しない場合、falseを返す', () => { - jest.spyOn(fs, 'existsSync').mockReturnValue(false); - - const result = fileExists('/path/to/nonexistent.json'); - expect(result).toBe(false); - }); - - test('エラーが発生した場合、falseを返す', () => { - jest.spyOn(fs, 'existsSync').mockImplementation(() => { - throw new Error('アクセス拒否'); - }); - - const result = fileExists('/path/to/file.json'); - expect(result).toBe(false); - }); - }); - - describe('readJsonFile', () => { - test('ファイルが存在する場合、JSONオブジェクトを返す', () => { - const mockData = { key: 'value' }; - jest.spyOn(fs, 'existsSync').mockReturnValue(true); - jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(mockData)); - - const result = readJsonFile('/path/to/file.json'); - expect(result).toEqual(mockData); - expect(fs.readFileSync).toHaveBeenCalledWith( - '/path/to/file.json', - 'utf-8', - ); - }); - - test('ファイルが存在しない場合、nullを返す', () => { - jest.spyOn(fs, 'existsSync').mockReturnValue(false); - - const result = readJsonFile('/path/to/nonexistent.json'); - expect(result).toBeNull(); - expect(fs.readFileSync).not.toHaveBeenCalled(); - }); - - test('ファイル読み込みでエラーが発生した場合、例外をスローする', () => { - jest.spyOn(fs, 'existsSync').mockReturnValue(true); - jest.spyOn(fs, 'readFileSync').mockImplementation(() => { - throw new Error('読み込みエラー'); - }); - - expect(() => readJsonFile('/path/to/file.json')).toThrow(); - }); - - test('JSONパースでエラーが発生した場合、例外をスローする', () => { - jest.spyOn(fs, 'existsSync').mockReturnValue(true); - jest.spyOn(fs, 'readFileSync').mockReturnValue('不正なJSON'); - - expect(() => readJsonFile('/path/to/file.json')).toThrow(); - }); - }); - - describe('resolveProjectPath', () => { - test('相対パスを絶対パスに変換する', () => { - // パスの解決をモック - const mockProjectRoot = '/mock/project/root'; - jest.spyOn(path, 'resolve').mockImplementation((...args) => { - // 単純化のため、引数を連結して返す - return args.join('/'); - }); - - const result = resolveProjectPath('config/file.json'); - - // 最終的な結果のみを検証 - expect(result).toContain('config/file.json'); - }); - }); -}); diff --git a/src/utils/__tests__/logger.test.ts b/src/utils/__tests__/logger.test.ts deleted file mode 100644 index 1ed92ef..0000000 --- a/src/utils/__tests__/logger.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { createLogger } from '../logger'; -import { Config } from '../../config'; -import winston from 'winston'; - -// winstonをモック化 -jest.mock('winston', () => { - const mockFormat = { - combine: jest.fn().mockReturnThis(), - colorize: jest.fn().mockReturnThis(), - timestamp: jest.fn().mockReturnThis(), - printf: jest.fn().mockReturnThis(), - json: jest.fn().mockReturnThis(), - }; - - const mockLogger = { - info: jest.fn(), - error: jest.fn(), - warn: jest.fn(), - debug: jest.fn(), - }; - - return { - format: mockFormat, - transports: { - Console: jest.fn(), - File: jest.fn(), - }, - createLogger: jest.fn().mockReturnValue(mockLogger), - }; -}); - -describe('ロガー', () => { - let config: Config; - - beforeEach(() => { - // モックのリセット - jest.clearAllMocks(); - - // 設定オブジェクトの作成 - config = { - port: 8080, - host: 'localhost', - ignoreRobotsTxt: false, - llm: { - type: 'ollama', - enabled: true, - model: 'gemma', - }, - logging: { - level: 'info', - }, - } as Config; - }); - - test('ロガーが正しく作成される', () => { - const logger = createLogger(config); - expect(winston.createLogger).toHaveBeenCalledWith({ - level: config.logging.level, - transports: expect.any(Array), - }); - }); - - test('ファイルロギングが設定されている場合', () => { - const configWithFile = { ...config }; - configWithFile.logging.file = 'logs/app.log'; - - createLogger(configWithFile); - expect(winston.transports.File).toHaveBeenCalledWith({ - filename: configWithFile.logging.file, - format: expect.any(Object), - }); - }); - - test('ロガーが正しいレベルで作成される', () => { - config.logging.level = 'debug'; - createLogger(config); - expect(winston.createLogger).toHaveBeenCalledWith( - expect.objectContaining({ - level: 'debug', - }), - ); - }); -}); diff --git a/src/utils/__tests__/robots-txt.test.ts b/src/utils/__tests__/robots-txt.test.ts deleted file mode 100644 index 043b673..0000000 --- a/src/utils/__tests__/robots-txt.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * robots.txtマネージャーのテスト - */ - -import { RobotsTxtManager } from '../robots-txt'; -import { ConfigError } from '../errors'; -import fetchMock from 'jest-fetch-mock'; - -describe('RobotsTxtManager', () => { - let manager: RobotsTxtManager; - - beforeEach(() => { - fetchMock.resetMocks(); - manager = new RobotsTxtManager({ - port: 8080, - host: 'localhost', - ignoreRobotsTxt: false, - llm: { - enabled: false, - type: 'ollama', - model: 'gemma', - }, - logging: { - level: 'error', - }, - }); - }); - - describe('robots.txtの取得と解析', () => { - const sampleRobotsTxt = ` - User-agent: * - Disallow: /private/ - Allow: /public/ - Crawl-delay: 1 - - User-agent: Googlebot - Allow: / - Disallow: /admin/ - `; - - test('robots.txtを正しく取得して解析する', async () => { - fetchMock.mockResponseOnce(sampleRobotsTxt); - - const result = await manager.getRobotsTxt('example.com'); - expect(result).toBeTruthy(); - expect(result?.rules).toHaveLength(2); - - const wildcardRule = result?.rules[0]; - expect(wildcardRule?.userAgent).toBe('*'); - expect(wildcardRule?.disallow).toContain('/private/'); - expect(wildcardRule?.allow).toContain('/public/'); - expect(wildcardRule?.crawlDelay).toBe(1); - - const googlebotRule = result?.rules[1]; - expect(googlebotRule?.userAgent).toBe('googlebot'); - expect(googlebotRule?.allow).toContain('/'); - expect(googlebotRule?.disallow).toContain('/admin/'); - }); - - test('robots.txtが存在しない場合はnullを返す', async () => { - fetchMock.mockResponseOnce('', { status: 404 }); - - const result = await manager.getRobotsTxt('example.com'); - expect(result).toBeNull(); - }); - - test('robots.txtの取得に失敗した場合はエラーを投げる', async () => { - fetchMock.mockResponseOnce('', { status: 500 }); - - await expect(manager.getRobotsTxt('example.com')).rejects.toThrow(ConfigError); - }); - - test('キャッシュが正しく動作する', async () => { - fetchMock.mockResponseOnce(sampleRobotsTxt); - - // 1回目の取得 - const result1 = await manager.getRobotsTxt('example.com'); - expect(result1).toBeTruthy(); - expect(fetchMock).toHaveBeenCalledTimes(1); - - // キャッシュから取得(新しいリクエストは発生しない) - const result2 = await manager.getRobotsTxt('example.com'); - expect(result2).toBeTruthy(); - expect(fetchMock).toHaveBeenCalledTimes(1); - - // forceRefreshでキャッシュを無視 - const result3 = await manager.getRobotsTxt('example.com', true); - expect(result3).toBeTruthy(); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); - }); - - describe('パスのブロック判定', () => { - const sampleRobotsTxt = ` - User-agent: * - Disallow: /private/ - Allow: /private/public/ - Disallow: /*.pdf$ - - User-agent: Googlebot - Allow: / - Disallow: /admin/ - `; - - beforeEach(() => { - fetchMock.mockResponseOnce(sampleRobotsTxt); - }); - - test('ワイルドカードルールが正しく適用される', async () => { - expect(await manager.isBlocked('example.com', '/private/secret', 'TestBot')).toBe(true); - expect(await manager.isBlocked('example.com', '/private/public/page', 'TestBot')).toBe(false); - expect(await manager.isBlocked('example.com', '/public/page', 'TestBot')).toBe(false); - expect(await manager.isBlocked('example.com', '/document.pdf', 'TestBot')).toBe(true); - }); - - test('特定のUser-Agentのルールが優先される', async () => { - expect(await manager.isBlocked('example.com', '/private/secret', 'Googlebot')).toBe(false); - expect(await manager.isBlocked('example.com', '/admin/panel', 'Googlebot')).toBe(true); - }); - - test('robots.txtが存在しない場合はブロックしない', async () => { - fetchMock.mockResponseOnce('', { status: 404 }); - expect(await manager.isBlocked('example.com', '/any/path', 'TestBot')).toBe(false); - }); - }); - - describe('キャッシュ管理', () => { - const sampleRobotsTxt = `User-agent: *\nDisallow: /`; - - test('特定のドメインのキャッシュをクリアする', async () => { - fetchMock.mockResponseOnce(sampleRobotsTxt); - - await manager.getRobotsTxt('example.com'); - expect(fetchMock).toHaveBeenCalledTimes(1); - - manager.clearCache('example.com'); - - await manager.getRobotsTxt('example.com'); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); - - test('全てのキャッシュをクリアする', async () => { - fetchMock - .mockResponseOnce(sampleRobotsTxt) // example.com用 - .mockResponseOnce(sampleRobotsTxt); // test.com用 - - await manager.getRobotsTxt('example.com'); - await manager.getRobotsTxt('test.com'); - expect(fetchMock).toHaveBeenCalledTimes(2); - - manager.clearCache(); - - await manager.getRobotsTxt('example.com'); - await manager.getRobotsTxt('test.com'); - expect(fetchMock).toHaveBeenCalledTimes(4); - }); - }); -}); diff --git a/src/utils/__tests__/user-agent.test.ts b/src/utils/__tests__/user-agent.test.ts deleted file mode 100644 index 83f2255..0000000 --- a/src/utils/__tests__/user-agent.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * User-Agent管理モジュールのテスト - */ - -import { UserAgentManager } from '../user-agent'; - -describe('UserAgentManager', () => { - describe('基本機能', () => { - test('デフォルト設定で初期化できる', () => { - const manager = new UserAgentManager({ - enabled: true, - rotate: false, - }); - - expect(manager.getCurrentUserAgent()).toBeDefined(); - }); - - test('無効化時はundefinedを返す', () => { - const manager = new UserAgentManager({ - enabled: false, - rotate: false, - }); - - expect(manager.getCurrentUserAgent()).toBeUndefined(); - }); - }); - - describe('カスタム設定', () => { - test('カスタムUser-Agentを使用できる', () => { - const customUA = 'Custom User Agent'; - const manager = new UserAgentManager({ - enabled: true, - rotate: false, - value: customUA, - }); - - expect(manager.getCurrentUserAgent()).toBe(customUA); - }); - - test('プリセットUser-Agentを設定できる', () => { - const presets = ['UA1', 'UA2', 'UA3']; - const manager = new UserAgentManager({ - enabled: true, - rotate: false, - presets, - }); - - expect(presets).toContain(manager.getCurrentUserAgent()); - }); - }); - - describe('ローテーション機能', () => { - test('User-Agentをローテーションできる', () => { - const presets = ['UA1', 'UA2', 'UA3']; - const manager = new UserAgentManager({ - enabled: true, - rotate: true, - presets, - }); - - const firstUA = manager.getCurrentUserAgent(); - const secondUA = manager.getCurrentUserAgent(); - const thirdUA = manager.getCurrentUserAgent(); - const fourthUA = manager.getCurrentUserAgent(); - - expect(firstUA).not.toBe(secondUA); - expect(secondUA).not.toBe(thirdUA); - expect(fourthUA).toBe(firstUA); // 一周して最初に戻る - }); - }); - - describe('User-Agent管理', () => { - test('新しいUser-Agentを追加できる', () => { - const manager = new UserAgentManager({ - enabled: true, - rotate: true, - }); - - const newUA = 'New User Agent'; - manager.addUserAgent(newUA); - - // ローテーションを一周させて新しいUAが含まれているか確認 - let found = false; - for (let i = 0; i < 10; i++) { - if (manager.getCurrentUserAgent() === newUA) { - found = true; - break; - } - } - - expect(found).toBe(true); - }); - - test('リセット機能が正しく動作する', () => { - const presets = ['UA1', 'UA2']; - const manager = new UserAgentManager({ - enabled: true, - rotate: true, - presets, - }); - - // プリセットに含まれていないUser-Agentを追加 - const newUA = 'New User Agent'; - manager.addUserAgent(newUA); - - // リセット前に新しいUser-Agentが含まれていることを確認 - let foundBefore = false; - for (let i = 0; i < 10; i++) { - if (manager.getCurrentUserAgent() === newUA) { - foundBefore = true; - break; - } - } - expect(foundBefore).toBe(true); - - // リセット後 - manager.reset(); - - // リセット後は元のプリセットのみが使用されることを確認 - const usedUAs = new Set(); - for (let i = 0; i < 10; i++) { - const ua = manager.getCurrentUserAgent(); - usedUAs.add(ua!); - } - - expect(usedUAs.size).toBeLessThanOrEqual(presets.length); - expect(usedUAs.has(newUA)).toBe(false); - for (const ua of usedUAs) { - expect(presets).toContain(ua); - } - }); - }); -}); diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 6382129..6587973 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -15,7 +15,7 @@ export class AppError extends Error { constructor( message: string, - code: string = 'APP_ERROR', + codeOrMetadata: string | Record = 'APP_ERROR', statusCode = 500, metadata?: Record, isOperational = true, @@ -23,7 +23,13 @@ export class AppError extends Error { ) { super(message); this.name = 'AppError'; - this.code = code; + if (typeof codeOrMetadata === 'string') { + this.code = codeOrMetadata; + this.metadata = metadata; + } else { + this.code = 'APP_ERROR'; + this.metadata = codeOrMetadata; + } this.statusCode = statusCode; this.metadata = metadata; this.isOperational = isOperational;