Skip to content

Commit 2ac9cb3

Browse files
ci: e2e test (#142)
1 parent 59e54a4 commit 2ac9cb3

File tree

8 files changed

+272
-0
lines changed

8 files changed

+272
-0
lines changed

.github/workflows/e2e.yaml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: e2e
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- "renovate/**"
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: e2e
12+
cancel-in-progress: false
13+
14+
jobs:
15+
e2e-test:
16+
runs-on: ubuntu-latest
17+
strategy:
18+
max-parallel: 1
19+
matrix:
20+
runtime: [docker, npm]
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v5
25+
26+
- name: Setup pnpm
27+
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4
28+
29+
- name: Setup Node.js
30+
uses: actions/setup-node@v5
31+
with:
32+
node-version-file: .node-version
33+
cache: "pnpm"
34+
35+
- name: Install dependencies
36+
run: pnpm install --frozen-lockfile
37+
38+
# Docker runtime setup
39+
- name: Set up Docker Buildx
40+
if: matrix.runtime == 'docker'
41+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
42+
43+
- name: Build Docker image
44+
if: matrix.runtime == 'docker'
45+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
46+
with:
47+
context: .
48+
file: ./docker/Dockerfile
49+
tags: kintone-mcp-server:e2e
50+
load: true
51+
env:
52+
DOCKER_BUILD_SUMMARY: false
53+
54+
# npm runtime setup
55+
- name: Install @kintone/mcp-server
56+
if: matrix.runtime == 'npm'
57+
run: |
58+
pnpm build
59+
pnpm add -g .
60+
61+
# E2E test execution
62+
- name: Run E2E tests
63+
env:
64+
RUNTIME: ${{ matrix.runtime }}
65+
APP_ID: ${{ secrets.E2E_APP_ID }}
66+
KINTONE_BASE_URL: ${{ secrets.E2E_KINTONE_BASE_URL }}
67+
KINTONE_USERNAME: ${{ secrets.E2E_KINTONE_USERNAME }}
68+
KINTONE_PASSWORD: ${{ secrets.E2E_KINTONE_PASSWORD }}
69+
run: |
70+
pnpm test:e2e

e2e/client.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2+
import type { ProvidedConfig } from "../src/config/types/config.js";
3+
import { Client } from "@modelcontextprotocol/sdk/client";
4+
5+
export const createClient = async (transport: StdioClientTransport) => {
6+
const client = new Client({
7+
name: "e2e-client",
8+
version: "1.0.0",
9+
});
10+
await client.connect(transport);
11+
return client;
12+
};
13+
14+
export type Runtime = "docker" | "npm";
15+
16+
export const createTransport = (
17+
runtime: Runtime,
18+
config: ProvidedConfig,
19+
): StdioClientTransport => {
20+
switch (runtime) {
21+
case "docker":
22+
return createDockerTransport(config);
23+
case "npm":
24+
return createNpmTransport(config);
25+
default:
26+
throw new Error(`Unknown runtime: ${runtime}`);
27+
}
28+
};
29+
30+
export const createDockerTransport = (config: ProvidedConfig) => {
31+
return new StdioClientTransport({
32+
command: "docker",
33+
args: [
34+
"run",
35+
"-i",
36+
"--rm",
37+
// -eフラグを展開
38+
// e.g. "-e", "KINTONE_BASE_URL", "-e", "KINTONE_API_TOKEN", ...
39+
...Object.keys(config).flatMap((key) => ["-e", key]),
40+
"kintone-mcp-server:e2e",
41+
],
42+
env: {
43+
PATH: "/usr/local/bin:/usr/bin:/bin",
44+
...config,
45+
},
46+
});
47+
};
48+
49+
const ENV_TO_CLI_ARG = {
50+
KINTONE_BASE_URL: "--base-url",
51+
KINTONE_USERNAME: "--username",
52+
KINTONE_PASSWORD: "--password",
53+
KINTONE_API_TOKEN: "--api-token",
54+
KINTONE_BASIC_AUTH_USERNAME: "--basic-auth-username",
55+
KINTONE_BASIC_AUTH_PASSWORD: "--basic-auth-password",
56+
HTTPS_PROXY: "--proxy",
57+
KINTONE_PFX_FILE_PATH: "--pfx-file-path",
58+
KINTONE_PFX_FILE_PASSWORD: "--pfx-file-password",
59+
KINTONE_ATTACHMENTS_DIR: "--attachments-dir",
60+
} as const;
61+
62+
const isEnvKey = (key: string): key is keyof typeof ENV_TO_CLI_ARG =>
63+
key in ENV_TO_CLI_ARG;
64+
65+
export const createNpmTransport = (config: ProvidedConfig) => {
66+
const args: string[] = ["kintone-mcp-server"];
67+
68+
for (const [envKey, value] of Object.entries(config)) {
69+
if (value !== undefined && isEnvKey(envKey)) {
70+
args.push(ENV_TO_CLI_ARG[envKey], value);
71+
}
72+
}
73+
74+
return new StdioClientTransport({
75+
command: "npx",
76+
args,
77+
});
78+
};

e2e/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import z from "zod";
2+
3+
export const testConfigSchema = z.object({
4+
APP_ID: z.string(),
5+
RUNTIME: z.enum(["docker", "npm"]),
6+
});

e2e/install.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
2+
import type { Client } from "@modelcontextprotocol/sdk/client";
3+
import { createClient, createTransport } from "./client";
4+
import { configSchema } from "../src/config/schema";
5+
import { testConfigSchema } from "./config";
6+
import type { ProvidedConfig } from "../src/config/types/config";
7+
8+
describe("MCP Server Installation E2E Tests", () => {
9+
const mcpConfig = configSchema.parse(process.env);
10+
11+
const config: ProvidedConfig = {
12+
KINTONE_BASE_URL: mcpConfig.KINTONE_BASE_URL,
13+
KINTONE_USERNAME: mcpConfig.KINTONE_USERNAME,
14+
KINTONE_PASSWORD: mcpConfig.KINTONE_PASSWORD,
15+
};
16+
17+
const testConfig = testConfigSchema.parse(process.env);
18+
19+
const appId = testConfig.APP_ID;
20+
const runtime = testConfig.RUNTIME;
21+
22+
let client: Client;
23+
24+
beforeAll(async () => {
25+
const transport = createTransport(runtime, config);
26+
client = await createClient(transport);
27+
});
28+
29+
afterAll(async () => {
30+
await client.close();
31+
});
32+
33+
describe("Tool List Verification", () => {
34+
it("should list all kintone tools correctly", async () => {
35+
// Act
36+
const tools = await client.listTools();
37+
const toolNames = tools.tools.map((tool) => tool.name);
38+
39+
// Assert
40+
expect(tools.tools).toBeInstanceOf(Array);
41+
expect(tools.tools.length).toBeGreaterThan(0);
42+
43+
// 特定のツールが含まれていることを確認
44+
expect(toolNames).toContain("kintone-get-app");
45+
46+
// すべてのツールがkintoneプレフィックスを持つことを確認
47+
tools.tools.forEach((tool) => {
48+
expect(tool).toHaveProperty("name");
49+
expect(tool).toHaveProperty("description");
50+
expect(tool).toHaveProperty("inputSchema");
51+
expect(tool.name).toMatch(/^kintone-/);
52+
});
53+
});
54+
});
55+
56+
describe("Tool Execution Verification", () => {
57+
it("should execute kintone-get-app tool correctly", async () => {
58+
// Act
59+
const result = await client.callTool({
60+
name: "kintone-get-app",
61+
arguments: {
62+
appId: appId,
63+
},
64+
});
65+
66+
// Assert
67+
expect(result).not.toHaveProperty("isError");
68+
expect(result).toHaveProperty("content");
69+
expect(result).toHaveProperty("structuredContent");
70+
71+
// content配列の検証
72+
expect(result.content).toBeInstanceOf(Array);
73+
expect((result.content as unknown[]).length).toBeGreaterThan(0);
74+
expect((result.content as unknown[])[0]).toHaveProperty("type", "text");
75+
expect((result.content as unknown[])[0]).toHaveProperty("text");
76+
77+
// structuredContentの検証
78+
const structuredContent = result.structuredContent as Record<
79+
string,
80+
unknown
81+
>;
82+
expect(structuredContent).toHaveProperty("appId", appId);
83+
expect(structuredContent).toHaveProperty("name");
84+
expect(structuredContent).toHaveProperty("description");
85+
expect(structuredContent).toHaveProperty("createdAt");
86+
expect(structuredContent).toHaveProperty("creator");
87+
expect(structuredContent).toHaveProperty("modifiedAt");
88+
expect(structuredContent).toHaveProperty("modifier");
89+
});
90+
});
91+
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"start": "node dist/index.js",
5555
"test": "vitest",
5656
"test:coverage": "vitest --coverage",
57+
"test:e2e": "vitest --config vitest.config.e2e.ts",
5758
"test:watch": "vitest --watch",
5859
"typecheck": "tsc -noEmit "
5960
},

tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@
113113
"exclude": [
114114
"**/*.test.ts",
115115
"**/__tests__/**/*",
116+
"e2e",
117+
"vitest.config*.ts",
116118
"node_modules",
117119
"dist"
118120
]

vitest.config.e2e.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
// e2eフォルダのテストのみ実行
6+
include: ["e2e/**/*.test.ts"],
7+
exclude: ["**/node_modules/**", "**/dist/**", "**/build/**"],
8+
// 実環境を利用するため、並列実行を無効化して順次実行
9+
fileParallelism: false,
10+
pool: "forks",
11+
poolOptions: {
12+
forks: {
13+
singleFork: true,
14+
},
15+
},
16+
},
17+
});

vitest.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
exclude: ["**/node_modules/**", "**/dist/**", "**/build/**", "e2e/**"],
6+
},
7+
});

0 commit comments

Comments
 (0)