backlog-readonly-mcp-blog 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +57 -0
- package/.github/settings.yml +49 -0
- package/.husky/pre-commit +1 -0
- package/.kiro/hooks/agent-completion-sound.kiro.hook +15 -0
- package/.kiro/hooks/markdown-lint-save.kiro.hook +19 -0
- package/.kiro/settings/locale.json +3 -0
- package/.kiro/settings/mcp.json +32 -0
- package/.kiro/specs/backlog-readonly-mcp/design.md +285 -0
- package/.kiro/specs/backlog-readonly-mcp/requirements.md +158 -0
- package/.kiro/specs/backlog-readonly-mcp/tasks.md +224 -0
- package/.kiro/steering/blog-evaluation.md +103 -0
- package/.kiro/steering/pr-review-response.md +29 -0
- package/.markdownlint.json +5 -0
- package/.textlintrc.json +32 -0
- package/.vscode/settings.json +47 -0
- package/README.md +170 -0
- package/biome.json +36 -0
- package/blog_content/blog.md +57 -0
- package/package.json +49 -0
- package/packages/backlog-readonly-mcp/.backlog-mcp.env.example +49 -0
- package/packages/backlog-readonly-mcp/MCP_CLIENT_SETUP.md +303 -0
- package/packages/backlog-readonly-mcp/README.md +259 -0
- package/packages/backlog-readonly-mcp/package.json +58 -0
- package/packages/backlog-readonly-mcp/src/client/backlog-api-client.ts +348 -0
- package/packages/backlog-readonly-mcp/src/config/config-manager.ts +280 -0
- package/packages/backlog-readonly-mcp/src/index.ts +247 -0
- package/packages/backlog-readonly-mcp/src/tools/issue-tools.ts +449 -0
- package/packages/backlog-readonly-mcp/src/tools/master-data-tools.ts +209 -0
- package/packages/backlog-readonly-mcp/src/tools/project-tools.ts +219 -0
- package/packages/backlog-readonly-mcp/src/tools/tool-registry.ts +223 -0
- package/packages/backlog-readonly-mcp/src/tools/user-tools.ts +111 -0
- package/packages/backlog-readonly-mcp/src/tools/wiki-tools.ts +149 -0
- package/packages/backlog-readonly-mcp/src/types/index.ts +297 -0
- package/packages/backlog-readonly-mcp/src/utils/logger.ts +123 -0
- package/packages/backlog-readonly-mcp/test/backlog-api-client.test.ts +307 -0
- package/packages/backlog-readonly-mcp/test/config-manager.test.ts +303 -0
- package/packages/backlog-readonly-mcp/test/issue-tools.test.ts +345 -0
- package/packages/backlog-readonly-mcp/test/mcp-server-integration.test.ts +254 -0
- package/packages/backlog-readonly-mcp/test/mcp-server.test.ts +91 -0
- package/packages/backlog-readonly-mcp/test/project-tools.test.ts +194 -0
- package/packages/backlog-readonly-mcp/test/workspace-config.test.ts +320 -0
- package/packages/backlog-readonly-mcp/tsconfig.json +21 -0
- package/packages/backlog-readonly-mcp/tsconfig.prod.json +11 -0
- package/packages/backlog-readonly-mcp/vitest.config.ts +12 -0
- package/packages/cdk/README.md +14 -0
- package/packages/cdk/cdk.json +98 -0
- package/packages/cdk/jest.config.js +9 -0
- package/packages/cdk/node_modules/.bin/browserslist +21 -0
- package/packages/cdk/node_modules/.bin/cdk +21 -0
- package/packages/cdk/node_modules/.bin/jest +21 -0
- package/packages/cdk/node_modules/.bin/ts-jest +21 -0
- package/packages/cdk/node_modules/.bin/ts-node +21 -0
- package/packages/cdk/node_modules/.bin/ts-node-cwd +21 -0
- package/packages/cdk/node_modules/.bin/ts-node-esm +21 -0
- package/packages/cdk/node_modules/.bin/ts-node-script +21 -0
- package/packages/cdk/node_modules/.bin/ts-node-transpile-only +21 -0
- package/packages/cdk/node_modules/.bin/ts-script +21 -0
- package/packages/cdk/node_modules/.bin/tsc +21 -0
- package/packages/cdk/node_modules/.bin/tsserver +21 -0
- package/packages/cdk/package.json +31 -0
- package/packages/cdk/test/__snapshots__/cdk.test.ts.snap +40 -0
- package/packages/cdk/tsconfig.json +25 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/setup-blog.js +148 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* プロジェクトツールのテスト
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { BacklogApiClient } from '../src/client/backlog-api-client.js';
|
|
7
|
+
import { ConfigManager } from '../src/config/config-manager.js';
|
|
8
|
+
import { registerProjectTools } from '../src/tools/project-tools.js';
|
|
9
|
+
import { ToolRegistry } from '../src/tools/tool-registry.js';
|
|
10
|
+
import type { BacklogProject, BacklogUser } from '../src/types/index.js';
|
|
11
|
+
|
|
12
|
+
// モックデータ
|
|
13
|
+
const mockProject: BacklogProject = {
|
|
14
|
+
id: 1,
|
|
15
|
+
projectKey: 'TEST',
|
|
16
|
+
name: 'テストプロジェクト',
|
|
17
|
+
chartEnabled: true,
|
|
18
|
+
subtaskingEnabled: true,
|
|
19
|
+
projectLeaderCanEditProjectLeader: false,
|
|
20
|
+
useWikiTreeView: true,
|
|
21
|
+
textFormattingRule: 'markdown',
|
|
22
|
+
archived: false,
|
|
23
|
+
displayOrder: 1,
|
|
24
|
+
useDevAttributes: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const mockProjects: BacklogProject[] = [mockProject];
|
|
28
|
+
|
|
29
|
+
const mockUsers: BacklogUser[] = [
|
|
30
|
+
{
|
|
31
|
+
id: 1,
|
|
32
|
+
userId: 'test-user',
|
|
33
|
+
name: 'テストユーザー',
|
|
34
|
+
roleType: 1,
|
|
35
|
+
lang: 'ja',
|
|
36
|
+
mailAddress: 'test@example.com',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
describe('プロジェクトツール', () => {
|
|
41
|
+
let toolRegistry: ToolRegistry;
|
|
42
|
+
let apiClient: BacklogApiClient;
|
|
43
|
+
let configManager: ConfigManager;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
// ConfigManagerのリセット
|
|
47
|
+
configManager = ConfigManager.getInstance();
|
|
48
|
+
configManager.reset();
|
|
49
|
+
|
|
50
|
+
// 環境変数の設定
|
|
51
|
+
process.env.BACKLOG_DOMAIN = 'test.backlog.com';
|
|
52
|
+
process.env.BACKLOG_API_KEY = 'test-api-key';
|
|
53
|
+
process.env.BACKLOG_DEFAULT_PROJECT = 'TEST';
|
|
54
|
+
|
|
55
|
+
// 設定の読み込み
|
|
56
|
+
configManager.loadConfig();
|
|
57
|
+
|
|
58
|
+
// APIクライアントのモック
|
|
59
|
+
apiClient = new BacklogApiClient(configManager);
|
|
60
|
+
vi.spyOn(apiClient, 'get').mockImplementation(async (endpoint: string) => {
|
|
61
|
+
if (endpoint === '/projects') {
|
|
62
|
+
return mockProjects;
|
|
63
|
+
}
|
|
64
|
+
if (endpoint === '/projects/TEST') {
|
|
65
|
+
return mockProject;
|
|
66
|
+
}
|
|
67
|
+
if (endpoint === '/projects/TEST/users') {
|
|
68
|
+
return mockUsers;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Unknown endpoint: ${endpoint}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ツールレジストリの初期化
|
|
74
|
+
toolRegistry = new ToolRegistry();
|
|
75
|
+
registerProjectTools(toolRegistry, apiClient);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('get_projects', () => {
|
|
79
|
+
it('プロジェクト一覧を取得できる', async () => {
|
|
80
|
+
const result = await toolRegistry.executeTool('get_projects', {});
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
success: true,
|
|
84
|
+
data: mockProjects,
|
|
85
|
+
count: 1,
|
|
86
|
+
message: '1件のプロジェクトを取得しました',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('アーカイブされたプロジェクトも含めて取得できる', async () => {
|
|
91
|
+
const result = await toolRegistry.executeTool('get_projects', {
|
|
92
|
+
archived: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(result).toEqual({
|
|
96
|
+
success: true,
|
|
97
|
+
data: mockProjects,
|
|
98
|
+
count: 1,
|
|
99
|
+
message: '1件のプロジェクトを取得しました',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('get_project', () => {
|
|
105
|
+
it('プロジェクト詳細を取得できる', async () => {
|
|
106
|
+
const result = await toolRegistry.executeTool('get_project', {
|
|
107
|
+
projectIdOrKey: 'TEST',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(result).toMatchObject({
|
|
111
|
+
success: true,
|
|
112
|
+
data: mockProject,
|
|
113
|
+
message: 'プロジェクト "テストプロジェクト" の詳細情報を取得しました',
|
|
114
|
+
isDefaultProject: false,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('デフォルトプロジェクトを使用してプロジェクト詳細を取得できる', async () => {
|
|
119
|
+
const result = await toolRegistry.executeTool('get_project', {});
|
|
120
|
+
|
|
121
|
+
expect(result).toMatchObject({
|
|
122
|
+
success: true,
|
|
123
|
+
data: mockProject,
|
|
124
|
+
message:
|
|
125
|
+
'プロジェクト "テストプロジェクト" の詳細情報を取得しました(デフォルトプロジェクト)',
|
|
126
|
+
isDefaultProject: true,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('get_project_users', () => {
|
|
132
|
+
it('プロジェクトメンバーを取得できる', async () => {
|
|
133
|
+
const result = await toolRegistry.executeTool('get_project_users', {
|
|
134
|
+
projectIdOrKey: 'TEST',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(result).toMatchObject({
|
|
138
|
+
success: true,
|
|
139
|
+
data: mockUsers,
|
|
140
|
+
count: 1,
|
|
141
|
+
message: 'プロジェクトのメンバー 1名を取得しました',
|
|
142
|
+
isDefaultProject: false,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('デフォルトプロジェクトを使用してプロジェクトメンバーを取得できる', async () => {
|
|
147
|
+
const result = await toolRegistry.executeTool('get_project_users', {});
|
|
148
|
+
|
|
149
|
+
expect(result).toMatchObject({
|
|
150
|
+
success: true,
|
|
151
|
+
data: mockUsers,
|
|
152
|
+
count: 1,
|
|
153
|
+
message:
|
|
154
|
+
'プロジェクトのメンバー 1名を取得しました(デフォルトプロジェクト)',
|
|
155
|
+
isDefaultProject: true,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('get_default_project', () => {
|
|
161
|
+
it('デフォルトプロジェクト情報を取得できる', async () => {
|
|
162
|
+
const result = await toolRegistry.executeTool('get_default_project', {});
|
|
163
|
+
|
|
164
|
+
expect(result).toMatchObject({
|
|
165
|
+
success: true,
|
|
166
|
+
data: mockProject,
|
|
167
|
+
message:
|
|
168
|
+
'デフォルトプロジェクト "テストプロジェクト" の情報を取得しました',
|
|
169
|
+
defaultProjectKey: 'TEST',
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('ツール登録', () => {
|
|
175
|
+
it('すべてのプロジェクトツールが登録されている', () => {
|
|
176
|
+
const tools = toolRegistry.getTools();
|
|
177
|
+
const toolNames = tools.map((tool) => tool.name);
|
|
178
|
+
|
|
179
|
+
expect(toolNames).toContain('get_projects');
|
|
180
|
+
expect(toolNames).toContain('get_project');
|
|
181
|
+
expect(toolNames).toContain('get_project_users');
|
|
182
|
+
expect(toolNames).toContain('get_default_project');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('各ツールが適切な説明を持っている', () => {
|
|
186
|
+
const tools = toolRegistry.getTools();
|
|
187
|
+
|
|
188
|
+
for (const tool of tools) {
|
|
189
|
+
expect(tool.description).toBeTruthy();
|
|
190
|
+
expect(tool.description).toContain('読み取り専用');
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ワークスペース固有設定の動作確認テスト
|
|
3
|
+
*
|
|
4
|
+
* 要件 10.1, 10.2, 10.3 の検証
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
10
|
+
import { ConfigManager } from '../src/config/config-manager.js';
|
|
11
|
+
|
|
12
|
+
describe('Workspace Configuration Tests', () => {
|
|
13
|
+
const testConfigPath = join(process.cwd(), '.backlog-mcp.env.test');
|
|
14
|
+
const defaultConfigPath = join(process.cwd(), '.backlog-mcp.env');
|
|
15
|
+
let originalEnv: Record<string, string | undefined>;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// 環境変数を保存
|
|
19
|
+
originalEnv = {
|
|
20
|
+
BACKLOG_DOMAIN: process.env.BACKLOG_DOMAIN,
|
|
21
|
+
BACKLOG_API_KEY: process.env.BACKLOG_API_KEY,
|
|
22
|
+
BACKLOG_DEFAULT_PROJECT: process.env.BACKLOG_DEFAULT_PROJECT,
|
|
23
|
+
BACKLOG_MAX_RETRIES: process.env.BACKLOG_MAX_RETRIES,
|
|
24
|
+
BACKLOG_TIMEOUT: process.env.BACKLOG_TIMEOUT,
|
|
25
|
+
BACKLOG_CONFIG_PATH: process.env.BACKLOG_CONFIG_PATH,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ConfigManagerをリセット
|
|
29
|
+
ConfigManager.getInstance().reset();
|
|
30
|
+
|
|
31
|
+
// テスト用設定ファイルが存在する場合は削除
|
|
32
|
+
[testConfigPath, defaultConfigPath].forEach((path) => {
|
|
33
|
+
if (existsSync(path)) {
|
|
34
|
+
unlinkSync(path);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
// 環境変数を復元
|
|
41
|
+
Object.keys(originalEnv).forEach((key) => {
|
|
42
|
+
if (originalEnv[key] === undefined) {
|
|
43
|
+
delete process.env[key];
|
|
44
|
+
} else {
|
|
45
|
+
process.env[key] = originalEnv[key];
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// テスト用設定ファイルを削除
|
|
50
|
+
[testConfigPath, defaultConfigPath].forEach((path) => {
|
|
51
|
+
if (existsSync(path)) {
|
|
52
|
+
unlinkSync(path);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ConfigManagerをリセット
|
|
57
|
+
ConfigManager.getInstance().reset();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 要件 10.1: BACKLOG_CONFIG_PATH環境変数で指定されたファイルから設定を読み込む
|
|
62
|
+
*/
|
|
63
|
+
it('should load config from BACKLOG_CONFIG_PATH specified file', () => {
|
|
64
|
+
// システム環境変数を設定
|
|
65
|
+
process.env.BACKLOG_DOMAIN = 'system.backlog.com';
|
|
66
|
+
process.env.BACKLOG_API_KEY = 'system-api-key';
|
|
67
|
+
process.env.BACKLOG_DEFAULT_PROJECT = 'SYSTEM';
|
|
68
|
+
|
|
69
|
+
// カスタム設定ファイルを作成
|
|
70
|
+
const customConfig = [
|
|
71
|
+
'BACKLOG_DOMAIN=custom.backlog.com',
|
|
72
|
+
'BACKLOG_API_KEY=custom-api-key',
|
|
73
|
+
'BACKLOG_DEFAULT_PROJECT=CUSTOM',
|
|
74
|
+
'BACKLOG_MAX_RETRIES=5',
|
|
75
|
+
'BACKLOG_TIMEOUT=45000',
|
|
76
|
+
].join('\n');
|
|
77
|
+
writeFileSync(testConfigPath, customConfig);
|
|
78
|
+
|
|
79
|
+
// BACKLOG_CONFIG_PATH環境変数を設定
|
|
80
|
+
process.env.BACKLOG_CONFIG_PATH = testConfigPath;
|
|
81
|
+
|
|
82
|
+
const manager = ConfigManager.getInstance();
|
|
83
|
+
const config = manager.loadConfig();
|
|
84
|
+
|
|
85
|
+
// カスタム設定ファイルの値が使用されることを確認
|
|
86
|
+
// 認証情報は環境変数を優先(要件10.4)
|
|
87
|
+
expect(config.domain).toBe('system.backlog.com');
|
|
88
|
+
expect(config.apiKey).toBe('system-api-key');
|
|
89
|
+
// その他の設定はカスタム設定ファイルを優先
|
|
90
|
+
expect(config.defaultProject).toBe('CUSTOM');
|
|
91
|
+
expect(config.maxRetries).toBe(5);
|
|
92
|
+
expect(config.timeout).toBe(45000);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 要件 10.2: ワークスペース固有の設定ファイルが存在するとき、そのファイルからBACKLOG_DEFAULT_PROJECTを読み込む
|
|
97
|
+
*/
|
|
98
|
+
it('should load BACKLOG_DEFAULT_PROJECT from workspace config file', () => {
|
|
99
|
+
// システム環境変数を設定(デフォルトプロジェクトなし)
|
|
100
|
+
process.env.BACKLOG_DOMAIN = 'test.backlog.com';
|
|
101
|
+
process.env.BACKLOG_API_KEY = 'test-api-key';
|
|
102
|
+
|
|
103
|
+
// ワークスペース設定ファイルを作成(デフォルトの .backlog-mcp.env)
|
|
104
|
+
const workspaceConfig = ['BACKLOG_DEFAULT_PROJECT=WORKSPACE_PROJ'].join(
|
|
105
|
+
'\n',
|
|
106
|
+
);
|
|
107
|
+
writeFileSync(defaultConfigPath, workspaceConfig);
|
|
108
|
+
|
|
109
|
+
const manager = ConfigManager.getInstance();
|
|
110
|
+
const config = manager.loadConfig();
|
|
111
|
+
|
|
112
|
+
// ワークスペース設定からデフォルトプロジェクトが読み込まれることを確認
|
|
113
|
+
expect(config.defaultProject).toBe('WORKSPACE_PROJ');
|
|
114
|
+
expect(manager.hasDefaultProject()).toBe(true);
|
|
115
|
+
expect(manager.getDefaultProject()).toBe('WORKSPACE_PROJ');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 要件 10.3: デフォルトプロジェクトが設定されているとき、プロジェクトIDを省略した課題取得でそのプロジェクトを使用する
|
|
120
|
+
*/
|
|
121
|
+
it('should use default project when project ID is omitted', () => {
|
|
122
|
+
// システム環境変数を設定
|
|
123
|
+
process.env.BACKLOG_DOMAIN = 'test.backlog.com';
|
|
124
|
+
process.env.BACKLOG_API_KEY = 'test-api-key';
|
|
125
|
+
process.env.BACKLOG_DEFAULT_PROJECT = 'DEFAULT_PROJ';
|
|
126
|
+
|
|
127
|
+
// ワークスペース設定ファイルが存在しないことを確認
|
|
128
|
+
if (existsSync(defaultConfigPath)) {
|
|
129
|
+
unlinkSync(defaultConfigPath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const manager = ConfigManager.getInstance();
|
|
133
|
+
manager.reset(); // 確実にリセット
|
|
134
|
+
manager.loadConfig();
|
|
135
|
+
|
|
136
|
+
// プロジェクトIDを省略した場合、デフォルトプロジェクトが使用されることを確認
|
|
137
|
+
const resolvedProject = manager.resolveProjectIdOrKey();
|
|
138
|
+
expect(resolvedProject).toBe('DEFAULT_PROJ');
|
|
139
|
+
|
|
140
|
+
// 明示的にプロジェクトIDを指定した場合、そちらが優先されることを確認
|
|
141
|
+
const explicitProject = manager.resolveProjectIdOrKey('EXPLICIT_PROJ');
|
|
142
|
+
expect(explicitProject).toBe('EXPLICIT_PROJ');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 要件 10.4: 認証情報(BACKLOG_DOMAIN、BACKLOG_API_KEY)は環境変数から取得する
|
|
147
|
+
*/
|
|
148
|
+
it('should get authentication info from environment variables', () => {
|
|
149
|
+
// 環境変数のみで認証情報を設定
|
|
150
|
+
process.env.BACKLOG_DOMAIN = 'env.backlog.com';
|
|
151
|
+
process.env.BACKLOG_API_KEY = 'env-api-key';
|
|
152
|
+
|
|
153
|
+
// ワークスペース設定ファイルには認証情報以外を設定
|
|
154
|
+
const workspaceConfig = [
|
|
155
|
+
'BACKLOG_DEFAULT_PROJECT=WS_PROJ',
|
|
156
|
+
'BACKLOG_MAX_RETRIES=7',
|
|
157
|
+
].join('\n');
|
|
158
|
+
writeFileSync(defaultConfigPath, workspaceConfig);
|
|
159
|
+
|
|
160
|
+
// ConfigManagerをリセットして新しい設定を読み込み
|
|
161
|
+
ConfigManager.getInstance().reset();
|
|
162
|
+
const manager = ConfigManager.getInstance();
|
|
163
|
+
const config = manager.loadConfig();
|
|
164
|
+
|
|
165
|
+
// 認証情報は環境変数から取得されることを確認
|
|
166
|
+
expect(config.domain).toBe('env.backlog.com');
|
|
167
|
+
expect(config.apiKey).toBe('env-api-key');
|
|
168
|
+
// その他の設定はワークスペース設定から取得されることを確認
|
|
169
|
+
expect(config.defaultProject).toBe('WS_PROJ');
|
|
170
|
+
expect(config.maxRetries).toBe(7);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 要件 10.5: 設定ファイルが存在しないとき、環境変数のみを使用して動作する
|
|
175
|
+
*/
|
|
176
|
+
it('should work with environment variables only when config file does not exist', () => {
|
|
177
|
+
// 環境変数のみを設定
|
|
178
|
+
process.env.BACKLOG_DOMAIN = 'env-only.backlog.com';
|
|
179
|
+
process.env.BACKLOG_API_KEY = 'env-only-api-key';
|
|
180
|
+
process.env.BACKLOG_DEFAULT_PROJECT = 'ENV_PROJ';
|
|
181
|
+
process.env.BACKLOG_MAX_RETRIES = '2';
|
|
182
|
+
process.env.BACKLOG_TIMEOUT = '20000';
|
|
183
|
+
|
|
184
|
+
// 設定ファイルが存在しないことを確認
|
|
185
|
+
expect(existsSync(defaultConfigPath)).toBe(false);
|
|
186
|
+
|
|
187
|
+
const manager = ConfigManager.getInstance();
|
|
188
|
+
const config = manager.loadConfig();
|
|
189
|
+
|
|
190
|
+
// 環境変数の値が使用されることを確認
|
|
191
|
+
expect(config.domain).toBe('env-only.backlog.com');
|
|
192
|
+
expect(config.apiKey).toBe('env-only-api-key');
|
|
193
|
+
expect(config.defaultProject).toBe('ENV_PROJ');
|
|
194
|
+
expect(config.maxRetries).toBe(2);
|
|
195
|
+
expect(config.timeout).toBe(20000);
|
|
196
|
+
|
|
197
|
+
// 設定サマリーでワークスペース設定がないことを確認
|
|
198
|
+
const summary = manager.getConfigSummary();
|
|
199
|
+
expect(summary.hasWorkspaceConfig).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 要件 10.6: 設定ファイルの形式として.envファイル形式をサポートする
|
|
204
|
+
*/
|
|
205
|
+
it('should support .env file format', () => {
|
|
206
|
+
// 環境変数を完全にクリア
|
|
207
|
+
delete process.env.BACKLOG_DEFAULT_PROJECT;
|
|
208
|
+
delete process.env.BACKLOG_MAX_RETRIES;
|
|
209
|
+
delete process.env.BACKLOG_TIMEOUT;
|
|
210
|
+
|
|
211
|
+
// システム環境変数を設定(認証情報のみ)
|
|
212
|
+
process.env.BACKLOG_DOMAIN = 'test.backlog.com';
|
|
213
|
+
process.env.BACKLOG_API_KEY = 'test-api-key';
|
|
214
|
+
|
|
215
|
+
// .env形式の設定ファイルを作成(コメント、空行、クォート付き値を含む)
|
|
216
|
+
const envConfig = [
|
|
217
|
+
'# Backlog設定ファイル',
|
|
218
|
+
'',
|
|
219
|
+
'BACKLOG_DEFAULT_PROJECT=ENV_FORMAT_PROJ',
|
|
220
|
+
'BACKLOG_MAX_RETRIES="8"',
|
|
221
|
+
"BACKLOG_TIMEOUT='60000'",
|
|
222
|
+
'# コメント行',
|
|
223
|
+
'BACKLOG_CUSTOM_SETTING=value_with_equals=sign',
|
|
224
|
+
'',
|
|
225
|
+
].join('\n');
|
|
226
|
+
writeFileSync(defaultConfigPath, envConfig);
|
|
227
|
+
|
|
228
|
+
// ConfigManagerを完全にリセット
|
|
229
|
+
ConfigManager.getInstance().reset();
|
|
230
|
+
const manager = ConfigManager.getInstance();
|
|
231
|
+
const config = manager.loadConfig();
|
|
232
|
+
|
|
233
|
+
// .env形式が正しく解析されることを確認
|
|
234
|
+
expect(config.defaultProject).toBe('ENV_FORMAT_PROJ');
|
|
235
|
+
expect(config.maxRetries).toBe(8);
|
|
236
|
+
expect(config.timeout).toBe(60000);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 要件 10.7: 無効な設定ファイルが指定されたとき、適切なエラーメッセージを返す
|
|
241
|
+
*/
|
|
242
|
+
it('should handle invalid config file gracefully', () => {
|
|
243
|
+
// システム環境変数を設定
|
|
244
|
+
process.env.BACKLOG_DOMAIN = 'test.backlog.com';
|
|
245
|
+
process.env.BACKLOG_API_KEY = 'test-api-key';
|
|
246
|
+
|
|
247
|
+
// 存在しないファイルを指定
|
|
248
|
+
process.env.BACKLOG_CONFIG_PATH = '/non/existent/path/.backlog-mcp.env';
|
|
249
|
+
|
|
250
|
+
const manager = ConfigManager.getInstance();
|
|
251
|
+
|
|
252
|
+
// 存在しないファイルでもエラーにならず、環境変数で動作することを確認
|
|
253
|
+
expect(() => manager.loadConfig()).not.toThrow();
|
|
254
|
+
|
|
255
|
+
const config = manager.loadConfig();
|
|
256
|
+
expect(config.domain).toBe('test.backlog.com');
|
|
257
|
+
expect(config.apiKey).toBe('test-api-key');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* デフォルトプロジェクトが設定されていない場合のエラーハンドリング
|
|
262
|
+
*/
|
|
263
|
+
it('should throw error when no project ID and no default project', () => {
|
|
264
|
+
// デフォルトプロジェクトなしで設定
|
|
265
|
+
process.env.BACKLOG_DOMAIN = 'test.backlog.com';
|
|
266
|
+
process.env.BACKLOG_API_KEY = 'test-api-key';
|
|
267
|
+
|
|
268
|
+
const manager = ConfigManager.getInstance();
|
|
269
|
+
manager.loadConfig();
|
|
270
|
+
|
|
271
|
+
// プロジェクトIDを省略し、デフォルトプロジェクトもない場合はエラー
|
|
272
|
+
expect(() => manager.resolveProjectIdOrKey()).toThrow();
|
|
273
|
+
|
|
274
|
+
// エラーメッセージの内容を確認
|
|
275
|
+
try {
|
|
276
|
+
manager.resolveProjectIdOrKey();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
expect(error).toBeInstanceOf(Error);
|
|
279
|
+
expect((error as Error).message).toContain(
|
|
280
|
+
'プロジェクトIDまたはキーが指定されておらず、デフォルトプロジェクトも設定されていません',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 設定サマリー機能のテスト
|
|
287
|
+
*/
|
|
288
|
+
it('should provide configuration summary', () => {
|
|
289
|
+
// ConfigManagerをリセット
|
|
290
|
+
ConfigManager.getInstance().reset();
|
|
291
|
+
|
|
292
|
+
// 環境変数をクリア
|
|
293
|
+
delete process.env.BACKLOG_DOMAIN;
|
|
294
|
+
delete process.env.BACKLOG_API_KEY;
|
|
295
|
+
delete process.env.BACKLOG_DEFAULT_PROJECT;
|
|
296
|
+
delete process.env.BACKLOG_MAX_RETRIES;
|
|
297
|
+
delete process.env.BACKLOG_TIMEOUT;
|
|
298
|
+
|
|
299
|
+
// 環境変数を設定
|
|
300
|
+
process.env.BACKLOG_DOMAIN = 'summary.backlog.com';
|
|
301
|
+
process.env.BACKLOG_API_KEY = 'summary-api-key-12345678';
|
|
302
|
+
process.env.BACKLOG_DEFAULT_PROJECT = 'SUMMARY_PROJ';
|
|
303
|
+
|
|
304
|
+
// ワークスペース設定ファイルを作成
|
|
305
|
+
const workspaceConfig = 'BACKLOG_MAX_RETRIES=9';
|
|
306
|
+
writeFileSync(defaultConfigPath, workspaceConfig);
|
|
307
|
+
|
|
308
|
+
const manager = ConfigManager.getInstance();
|
|
309
|
+
manager.loadConfig();
|
|
310
|
+
|
|
311
|
+
const summary = manager.getConfigSummary();
|
|
312
|
+
|
|
313
|
+
expect(summary.domain).toBe('summary.backlog.com');
|
|
314
|
+
expect(summary.maskedApiKey).toMatch(/^summ\*+5678$/);
|
|
315
|
+
expect(summary.defaultProject).toBe('SUMMARY_PROJ');
|
|
316
|
+
expect(summary.maxRetries).toBe(9);
|
|
317
|
+
expect(summary.timeout).toBe(30000); // デフォルト値
|
|
318
|
+
expect(summary.hasWorkspaceConfig).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: 'node',
|
|
6
|
+
globals: true,
|
|
7
|
+
coverage: {
|
|
8
|
+
reporter: ['text', 'json', 'html'],
|
|
9
|
+
exclude: ['node_modules/', 'dist/', '**/*.test.ts', '**/*.config.ts'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Welcome to your CDK TypeScript project
|
|
2
|
+
|
|
3
|
+
This is a blank project for CDK development with TypeScript.
|
|
4
|
+
|
|
5
|
+
The `cdk.json` file tells the CDK Toolkit how to execute your app.
|
|
6
|
+
|
|
7
|
+
## Useful commands
|
|
8
|
+
|
|
9
|
+
* `npm run build` compile typescript to js
|
|
10
|
+
* `npm run watch` watch for changes and compile
|
|
11
|
+
* `npm run test` perform the jest unit tests
|
|
12
|
+
* `npx cdk deploy` deploy this stack to your default AWS account/region
|
|
13
|
+
* `npx cdk diff` compare deployed stack with current state
|
|
14
|
+
* `npx cdk synth` emits the synthesized CloudFormation template
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
|
|
3
|
+
"watch": {
|
|
4
|
+
"include": ["**"],
|
|
5
|
+
"exclude": [
|
|
6
|
+
"README.md",
|
|
7
|
+
"cdk*.json",
|
|
8
|
+
"**/*.d.ts",
|
|
9
|
+
"**/*.js",
|
|
10
|
+
"tsconfig.json",
|
|
11
|
+
"package*.json",
|
|
12
|
+
"yarn.lock",
|
|
13
|
+
"node_modules",
|
|
14
|
+
"test"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"context": {
|
|
18
|
+
"@aws-cdk/aws-signer:signingProfileNamePassedToCfn": true,
|
|
19
|
+
"@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": true,
|
|
20
|
+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
|
|
21
|
+
"@aws-cdk/core:checkSecretUsage": true,
|
|
22
|
+
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
|
|
23
|
+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
|
|
24
|
+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
|
|
25
|
+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
|
|
26
|
+
"@aws-cdk/aws-iam:minimizePolicies": true,
|
|
27
|
+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
|
|
28
|
+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
|
|
29
|
+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
|
|
30
|
+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
|
|
31
|
+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
|
|
32
|
+
"@aws-cdk/core:enablePartitionLiterals": true,
|
|
33
|
+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
|
|
34
|
+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
|
|
35
|
+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
|
|
36
|
+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
|
|
37
|
+
"@aws-cdk/aws-route53-patters:useCertificate": true,
|
|
38
|
+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
|
|
39
|
+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
|
|
40
|
+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
|
|
41
|
+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
|
|
42
|
+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
|
|
43
|
+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
|
|
44
|
+
"@aws-cdk/aws-redshift:columnId": true,
|
|
45
|
+
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
|
|
46
|
+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
|
|
47
|
+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
|
|
48
|
+
"@aws-cdk/aws-kms:aliasNameRef": true,
|
|
49
|
+
"@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": true,
|
|
50
|
+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
|
|
51
|
+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
|
|
52
|
+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
|
|
53
|
+
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
|
|
54
|
+
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
|
|
55
|
+
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
|
|
56
|
+
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
|
|
57
|
+
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
|
|
58
|
+
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
|
|
59
|
+
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
|
|
60
|
+
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
|
|
61
|
+
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
|
|
62
|
+
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
|
|
63
|
+
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
|
|
64
|
+
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
|
|
65
|
+
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
|
|
66
|
+
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
|
|
67
|
+
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
|
|
68
|
+
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
|
|
69
|
+
"@aws-cdk/core:explicitStackTags": true,
|
|
70
|
+
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
|
|
71
|
+
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
|
|
72
|
+
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
|
|
73
|
+
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
|
|
74
|
+
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
|
|
75
|
+
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
|
|
76
|
+
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
|
|
77
|
+
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
|
|
78
|
+
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
|
|
79
|
+
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
|
|
80
|
+
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
|
|
81
|
+
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
|
|
82
|
+
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
|
|
83
|
+
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
|
|
84
|
+
"@aws-cdk/core:enableAdditionalMetadataCollection": true,
|
|
85
|
+
"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false,
|
|
86
|
+
"@aws-cdk/aws-s3:setUniqueReplicationRoleName": true,
|
|
87
|
+
"@aws-cdk/aws-events:requireEventBusPolicySid": true,
|
|
88
|
+
"@aws-cdk/core:aspectPrioritiesMutating": true,
|
|
89
|
+
"@aws-cdk/aws-dynamodb:retainTableReplica": true,
|
|
90
|
+
"@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true,
|
|
91
|
+
"@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true,
|
|
92
|
+
"@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true,
|
|
93
|
+
"@aws-cdk/aws-s3:publicAccessBlockedByDefault": true,
|
|
94
|
+
"@aws-cdk/aws-lambda:useCdkManagedLogGroup": true,
|
|
95
|
+
"@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": true,
|
|
96
|
+
"@aws-cdk/aws-ecs-patterns:uniqueTargetGroupId": true
|
|
97
|
+
}
|
|
98
|
+
}
|