@kirrosh/apitool 0.4.3
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/workflows/ci.yml +27 -0
- package/.github/workflows/release.yml +97 -0
- package/.mcp.json +9 -0
- package/APITOOL.md +195 -0
- package/BACKLOG.md +62 -0
- package/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/bun.lock +291 -0
- package/docs/GLOSSARY.md +182 -0
- package/docs/INDEX.md +21 -0
- package/docs/agent.md +135 -0
- package/docs/archive/APITOOL-pre-M22.md +831 -0
- package/docs/archive/BACKLOG-AI-NATIVE.md +56 -0
- package/docs/archive/M1-M2-parser-runner.md +216 -0
- package/docs/archive/M4-M7-reporter-cli.md +179 -0
- package/docs/archive/M5-M7-storage-junit.md +300 -0
- package/docs/archive/M6-webui.md +339 -0
- package/docs/ci.md +274 -0
- package/docs/generation-issues.md +67 -0
- package/generated/.env.yaml +3 -0
- package/install.ps1 +80 -0
- package/install.sh +113 -0
- package/package.json +46 -0
- package/scripts/run-mocked-tests.ts +45 -0
- package/seed-demo.ts +53 -0
- package/self-tests/auth.yaml +18 -0
- package/self-tests/collections-crud.yaml +46 -0
- package/self-tests/environments-crud.yaml +48 -0
- package/self-tests/export.yaml +32 -0
- package/self-tests/runs.yaml +16 -0
- package/src/bun-types.d.ts +5 -0
- package/src/cli/commands/add-api.ts +51 -0
- package/src/cli/commands/ai-generate.ts +106 -0
- package/src/cli/commands/chat.ts +43 -0
- package/src/cli/commands/ci-init.ts +126 -0
- package/src/cli/commands/collections.ts +41 -0
- package/src/cli/commands/coverage.ts +65 -0
- package/src/cli/commands/doctor.ts +127 -0
- package/src/cli/commands/envs.ts +218 -0
- package/src/cli/commands/init.ts +84 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/run.ts +137 -0
- package/src/cli/commands/runs.ts +108 -0
- package/src/cli/commands/serve.ts +22 -0
- package/src/cli/commands/update.ts +142 -0
- package/src/cli/commands/validate.ts +18 -0
- package/src/cli/index.ts +500 -0
- package/src/cli/output.ts +24 -0
- package/src/cli/runtime.ts +7 -0
- package/src/core/agent/agent-loop.ts +116 -0
- package/src/core/agent/context-manager.ts +41 -0
- package/src/core/agent/system-prompt.ts +33 -0
- package/src/core/agent/tools/diagnose-failure.ts +51 -0
- package/src/core/agent/tools/explore-api.ts +40 -0
- package/src/core/agent/tools/index.ts +48 -0
- package/src/core/agent/tools/manage-environment.ts +40 -0
- package/src/core/agent/tools/query-results.ts +40 -0
- package/src/core/agent/tools/run-tests.ts +38 -0
- package/src/core/agent/tools/send-request.ts +44 -0
- package/src/core/agent/tools/validate-tests.ts +23 -0
- package/src/core/agent/types.ts +22 -0
- package/src/core/generator/ai/ai-generator.ts +61 -0
- package/src/core/generator/ai/llm-client.ts +159 -0
- package/src/core/generator/ai/output-parser.ts +307 -0
- package/src/core/generator/ai/prompt-builder.ts +153 -0
- package/src/core/generator/ai/types.ts +56 -0
- package/src/core/generator/coverage-scanner.ts +87 -0
- package/src/core/generator/data-factory.ts +115 -0
- package/src/core/generator/index.ts +10 -0
- package/src/core/generator/openapi-reader.ts +142 -0
- package/src/core/generator/schema-utils.ts +52 -0
- package/src/core/generator/serializer.ts +189 -0
- package/src/core/generator/types.ts +47 -0
- package/src/core/parser/filter.ts +14 -0
- package/src/core/parser/index.ts +21 -0
- package/src/core/parser/schema.ts +175 -0
- package/src/core/parser/types.ts +50 -0
- package/src/core/parser/variables.ts +146 -0
- package/src/core/parser/yaml-parser.ts +85 -0
- package/src/core/reporter/console.ts +175 -0
- package/src/core/reporter/index.ts +23 -0
- package/src/core/reporter/json.ts +9 -0
- package/src/core/reporter/junit.ts +78 -0
- package/src/core/reporter/types.ts +12 -0
- package/src/core/runner/assertions.ts +172 -0
- package/src/core/runner/execute-run.ts +75 -0
- package/src/core/runner/executor.ts +150 -0
- package/src/core/runner/http-client.ts +69 -0
- package/src/core/runner/index.ts +12 -0
- package/src/core/runner/types.ts +48 -0
- package/src/core/setup-api.ts +97 -0
- package/src/core/utils.ts +9 -0
- package/src/db/queries.ts +868 -0
- package/src/db/schema.ts +215 -0
- package/src/mcp/server.ts +47 -0
- package/src/mcp/tools/ci-init.ts +57 -0
- package/src/mcp/tools/coverage-analysis.ts +58 -0
- package/src/mcp/tools/explore-api.ts +84 -0
- package/src/mcp/tools/generate-missing-tests.ts +80 -0
- package/src/mcp/tools/generate-tests-guide.ts +353 -0
- package/src/mcp/tools/manage-environment.ts +123 -0
- package/src/mcp/tools/manage-server.ts +87 -0
- package/src/mcp/tools/query-db.ts +141 -0
- package/src/mcp/tools/run-tests.ts +66 -0
- package/src/mcp/tools/save-test-suite.ts +164 -0
- package/src/mcp/tools/send-request.ts +53 -0
- package/src/mcp/tools/setup-api.ts +49 -0
- package/src/mcp/tools/validate-tests.ts +42 -0
- package/src/tui/chat-ui.ts +150 -0
- package/src/web/routes/api.ts +234 -0
- package/src/web/routes/dashboard.ts +348 -0
- package/src/web/routes/runs.ts +64 -0
- package/src/web/schemas.ts +121 -0
- package/src/web/server.ts +134 -0
- package/src/web/static/htmx.min.js +1 -0
- package/src/web/static/style.css +265 -0
- package/src/web/views/layout.ts +46 -0
- package/src/web/views/results.ts +209 -0
- package/tests/agent/agent-loop.test.ts +61 -0
- package/tests/agent/context-manager.test.ts +59 -0
- package/tests/agent/system-prompt.test.ts +42 -0
- package/tests/agent/tools/diagnose-failure.test.ts +85 -0
- package/tests/agent/tools/explore-api.test.ts +59 -0
- package/tests/agent/tools/manage-environment.test.ts +78 -0
- package/tests/agent/tools/query-results.test.ts +77 -0
- package/tests/agent/tools/run-tests.test.ts +89 -0
- package/tests/agent/tools/send-request.test.ts +78 -0
- package/tests/agent/tools/validate-tests.test.ts +59 -0
- package/tests/ai/ai-generator.integration.test.ts +131 -0
- package/tests/ai/llm-client.test.ts +145 -0
- package/tests/ai/output-parser.test.ts +132 -0
- package/tests/ai/prompt-builder.test.ts +67 -0
- package/tests/ai/types.test.ts +55 -0
- package/tests/cli/args.test.ts +63 -0
- package/tests/cli/chat.test.ts +38 -0
- package/tests/cli/ci-init.test.ts +112 -0
- package/tests/cli/commands.test.ts +316 -0
- package/tests/cli/coverage.test.ts +58 -0
- package/tests/cli/doctor.test.ts +39 -0
- package/tests/cli/envs.test.ts +181 -0
- package/tests/cli/init.test.ts +80 -0
- package/tests/cli/runs.test.ts +94 -0
- package/tests/cli/safe-run.test.ts +103 -0
- package/tests/cli/update.test.ts +32 -0
- package/tests/core/generator/schema-utils.test.ts +108 -0
- package/tests/core/parser/nested-assertions.test.ts +80 -0
- package/tests/core/runner/root-body-assertions.test.ts +70 -0
- package/tests/db/chat-queries.test.ts +88 -0
- package/tests/db/chat-schema.test.ts +37 -0
- package/tests/db/environments.test.ts +131 -0
- package/tests/db/queries.test.ts +409 -0
- package/tests/db/schema.test.ts +141 -0
- package/tests/fixtures/.env.yaml +3 -0
- package/tests/fixtures/auth-token-test.yaml +8 -0
- package/tests/fixtures/bail/suite-a.yaml +6 -0
- package/tests/fixtures/bail/suite-b.yaml +6 -0
- package/tests/fixtures/crud.yaml +35 -0
- package/tests/fixtures/invalid-missing-name.yaml +5 -0
- package/tests/fixtures/invalid-no-method.yaml +6 -0
- package/tests/fixtures/petstore-auth.json +295 -0
- package/tests/fixtures/petstore-simple.json +151 -0
- package/tests/fixtures/post-only.yaml +12 -0
- package/tests/fixtures/simple.yaml +6 -0
- package/tests/fixtures/valid/.env.yaml +1 -0
- package/tests/fixtures/valid/a.yaml +5 -0
- package/tests/fixtures/valid/b.yml +5 -0
- package/tests/generator/coverage-scanner.test.ts +129 -0
- package/tests/generator/data-factory.test.ts +133 -0
- package/tests/generator/openapi-reader.test.ts +131 -0
- package/tests/integration/auth-flow.test.ts +217 -0
- package/tests/mcp/coverage-analysis.test.ts +64 -0
- package/tests/mcp/explore-api-schemas.test.ts +105 -0
- package/tests/mcp/explore-api.test.ts +49 -0
- package/tests/mcp/generate-missing-tests.test.ts +69 -0
- package/tests/mcp/manage-environment.test.ts +89 -0
- package/tests/mcp/save-test-suite.test.ts +116 -0
- package/tests/mcp/send-request.test.ts +79 -0
- package/tests/mcp/setup-api.test.ts +106 -0
- package/tests/mcp/tools.test.ts +248 -0
- package/tests/parser/schema.test.ts +134 -0
- package/tests/parser/variables.test.ts +227 -0
- package/tests/parser/yaml-parser.test.ts +69 -0
- package/tests/reporter/console.test.ts +256 -0
- package/tests/reporter/json.test.ts +98 -0
- package/tests/reporter/junit.test.ts +284 -0
- package/tests/runner/assertions.test.ts +262 -0
- package/tests/runner/executor.test.ts +310 -0
- package/tests/runner/http-client.test.ts +138 -0
- package/tests/web/routes.test.ts +160 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { generateFromSchema } from "../../src/core/generator/data-factory.ts";
|
|
3
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
4
|
+
|
|
5
|
+
describe("generateFromSchema", () => {
|
|
6
|
+
test("string returns placeholder", () => {
|
|
7
|
+
const result = generateFromSchema({ type: "string" } as OpenAPIV3.SchemaObject);
|
|
8
|
+
expect(result).toBe("{{$randomString}}");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("string with email format", () => {
|
|
12
|
+
const result = generateFromSchema({ type: "string", format: "email" } as OpenAPIV3.SchemaObject);
|
|
13
|
+
expect(result).toBe("{{$randomEmail}}");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("string with uuid format", () => {
|
|
17
|
+
const result = generateFromSchema({ type: "string", format: "uuid" } as OpenAPIV3.SchemaObject);
|
|
18
|
+
expect(result).toBe("{{$uuid}}");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("string with name property hint", () => {
|
|
22
|
+
const result = generateFromSchema({ type: "string" } as OpenAPIV3.SchemaObject, "name");
|
|
23
|
+
expect(result).toBe("{{$randomName}}");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("string with email property hint", () => {
|
|
27
|
+
const result = generateFromSchema({ type: "string" } as OpenAPIV3.SchemaObject, "email");
|
|
28
|
+
expect(result).toBe("{{$randomEmail}}");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("string with id property hint", () => {
|
|
32
|
+
const result = generateFromSchema({ type: "string" } as OpenAPIV3.SchemaObject, "userId");
|
|
33
|
+
expect(result).toBe("{{$uuid}}");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("integer returns randomInt", () => {
|
|
37
|
+
const result = generateFromSchema({ type: "integer" } as OpenAPIV3.SchemaObject);
|
|
38
|
+
expect(result).toBe("{{$randomInt}}");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("number returns randomInt", () => {
|
|
42
|
+
const result = generateFromSchema({ type: "number" } as OpenAPIV3.SchemaObject);
|
|
43
|
+
expect(result).toBe("{{$randomInt}}");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("boolean returns true", () => {
|
|
47
|
+
const result = generateFromSchema({ type: "boolean" } as OpenAPIV3.SchemaObject);
|
|
48
|
+
expect(result).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("enum returns first value", () => {
|
|
52
|
+
const result = generateFromSchema({
|
|
53
|
+
type: "string",
|
|
54
|
+
enum: ["available", "pending", "sold"],
|
|
55
|
+
} as OpenAPIV3.SchemaObject);
|
|
56
|
+
expect(result).toBe("available");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("object with properties", () => {
|
|
60
|
+
const schema: OpenAPIV3.SchemaObject = {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
name: { type: "string" } as OpenAPIV3.SchemaObject,
|
|
64
|
+
age: { type: "integer" } as OpenAPIV3.SchemaObject,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const result = generateFromSchema(schema) as Record<string, unknown>;
|
|
68
|
+
expect(result.name).toBe("{{$randomName}}");
|
|
69
|
+
expect(result.age).toBe("{{$randomInt}}");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("array with items", () => {
|
|
73
|
+
const schema: OpenAPIV3.SchemaObject = {
|
|
74
|
+
type: "array",
|
|
75
|
+
items: { type: "string" } as OpenAPIV3.SchemaObject,
|
|
76
|
+
};
|
|
77
|
+
const result = generateFromSchema(schema) as unknown[];
|
|
78
|
+
expect(result).toHaveLength(1);
|
|
79
|
+
expect(result[0]).toBe("{{$randomString}}");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("allOf merges schemas", () => {
|
|
83
|
+
const schema: OpenAPIV3.SchemaObject = {
|
|
84
|
+
allOf: [
|
|
85
|
+
{ type: "object", properties: { id: { type: "integer" } as OpenAPIV3.SchemaObject } } as OpenAPIV3.SchemaObject,
|
|
86
|
+
{ type: "object", properties: { name: { type: "string" } as OpenAPIV3.SchemaObject } } as OpenAPIV3.SchemaObject,
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
const result = generateFromSchema(schema) as Record<string, unknown>;
|
|
90
|
+
expect(result.id).toBe("{{$randomInt}}");
|
|
91
|
+
expect(result.name).toBe("{{$randomName}}");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("oneOf picks first variant", () => {
|
|
95
|
+
const schema: OpenAPIV3.SchemaObject = {
|
|
96
|
+
oneOf: [
|
|
97
|
+
{ type: "string" } as OpenAPIV3.SchemaObject,
|
|
98
|
+
{ type: "integer" } as OpenAPIV3.SchemaObject,
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
const result = generateFromSchema(schema);
|
|
102
|
+
expect(result).toBe("{{$randomString}}");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("anyOf picks first variant", () => {
|
|
106
|
+
const schema: OpenAPIV3.SchemaObject = {
|
|
107
|
+
anyOf: [
|
|
108
|
+
{ type: "integer" } as OpenAPIV3.SchemaObject,
|
|
109
|
+
{ type: "string" } as OpenAPIV3.SchemaObject,
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
const result = generateFromSchema(schema);
|
|
113
|
+
expect(result).toBe("{{$randomInt}}");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("nested object", () => {
|
|
117
|
+
const schema: OpenAPIV3.SchemaObject = {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
owner: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
name: { type: "string" } as OpenAPIV3.SchemaObject,
|
|
124
|
+
email: { type: "string", format: "email" } as OpenAPIV3.SchemaObject,
|
|
125
|
+
},
|
|
126
|
+
} as OpenAPIV3.SchemaObject,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const result = generateFromSchema(schema) as Record<string, any>;
|
|
130
|
+
expect(result.owner.name).toBe("{{$randomName}}");
|
|
131
|
+
expect(result.owner.email).toBe("{{$randomEmail}}");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../src/core/generator/openapi-reader.ts";
|
|
3
|
+
|
|
4
|
+
const FIXTURE = "tests/fixtures/petstore-auth.json";
|
|
5
|
+
|
|
6
|
+
describe("readOpenApiSpec", () => {
|
|
7
|
+
test("parses and dereferences petstore-auth spec", async () => {
|
|
8
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
9
|
+
expect(doc.openapi).toBe("3.0.0");
|
|
10
|
+
expect(doc.info.title).toBe("Test Petstore");
|
|
11
|
+
expect(doc.paths).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("contains securitySchemes component", async () => {
|
|
15
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
16
|
+
const schemes = doc.components?.securitySchemes;
|
|
17
|
+
expect(schemes).toBeDefined();
|
|
18
|
+
expect(schemes!.bearerAuth).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("extractSecuritySchemes", () => {
|
|
23
|
+
test("extracts bearer auth scheme", async () => {
|
|
24
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
25
|
+
const schemes = extractSecuritySchemes(doc);
|
|
26
|
+
|
|
27
|
+
expect(schemes.length).toBe(1);
|
|
28
|
+
expect(schemes[0]!.name).toBe("bearerAuth");
|
|
29
|
+
expect(schemes[0]!.type).toBe("http");
|
|
30
|
+
expect(schemes[0]!.scheme).toBe("bearer");
|
|
31
|
+
expect(schemes[0]!.bearerFormat).toBe("JWT");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("extractEndpoints", () => {
|
|
36
|
+
test("extracts all endpoints", async () => {
|
|
37
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
38
|
+
const endpoints = extractEndpoints(doc);
|
|
39
|
+
|
|
40
|
+
expect(endpoints.length).toBe(7); // login + 5 pet routes + health
|
|
41
|
+
|
|
42
|
+
const methods = endpoints.map((e) => `${e.method} ${e.path}`);
|
|
43
|
+
expect(methods).toContain("POST /auth/login");
|
|
44
|
+
expect(methods).toContain("GET /pets");
|
|
45
|
+
expect(methods).toContain("POST /pets");
|
|
46
|
+
expect(methods).toContain("GET /pets/{id}");
|
|
47
|
+
expect(methods).toContain("PUT /pets/{id}");
|
|
48
|
+
expect(methods).toContain("DELETE /pets/{id}");
|
|
49
|
+
expect(methods).toContain("GET /health");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("extracts operationId and summary", async () => {
|
|
53
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
54
|
+
const endpoints = extractEndpoints(doc);
|
|
55
|
+
|
|
56
|
+
const listPets = endpoints.find((e) => e.operationId === "listPets")!;
|
|
57
|
+
expect(listPets).toBeDefined();
|
|
58
|
+
expect(listPets.summary).toBe("List all pets");
|
|
59
|
+
expect(listPets.method).toBe("GET");
|
|
60
|
+
expect(listPets.path).toBe("/pets");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("extracts tags", async () => {
|
|
64
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
65
|
+
const endpoints = extractEndpoints(doc);
|
|
66
|
+
|
|
67
|
+
const getPet = endpoints.find((e) => e.operationId === "getPet")!;
|
|
68
|
+
expect(getPet.tags).toEqual(["pets"]);
|
|
69
|
+
|
|
70
|
+
const health = endpoints.find((e) => e.operationId === "healthCheck")!;
|
|
71
|
+
expect(health.tags).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("extracts parameters", async () => {
|
|
75
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
76
|
+
const endpoints = extractEndpoints(doc);
|
|
77
|
+
|
|
78
|
+
const getPet = endpoints.find((e) => e.operationId === "getPet")!;
|
|
79
|
+
expect(getPet.parameters.length).toBe(1);
|
|
80
|
+
expect(getPet.parameters[0]!.name).toBe("id");
|
|
81
|
+
expect(getPet.parameters[0]!.in).toBe("path");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("extracts request body schema", async () => {
|
|
85
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
86
|
+
const endpoints = extractEndpoints(doc);
|
|
87
|
+
|
|
88
|
+
const createPet = endpoints.find((e) => e.operationId === "createPet")!;
|
|
89
|
+
expect(createPet.requestBodySchema).toBeDefined();
|
|
90
|
+
expect(createPet.requestBodySchema!.type).toBe("object");
|
|
91
|
+
expect(createPet.requestBodySchema!.properties).toHaveProperty("name");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("extracts responses with schemas", async () => {
|
|
95
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
96
|
+
const endpoints = extractEndpoints(doc);
|
|
97
|
+
|
|
98
|
+
const getPet = endpoints.find((e) => e.operationId === "getPet")!;
|
|
99
|
+
expect(getPet.responses.length).toBe(2);
|
|
100
|
+
|
|
101
|
+
const ok = getPet.responses.find((r) => r.statusCode === 200)!;
|
|
102
|
+
expect(ok.description).toBe("A pet");
|
|
103
|
+
expect(ok.schema).toBeDefined();
|
|
104
|
+
expect(ok.schema!.type).toBe("object");
|
|
105
|
+
|
|
106
|
+
const notFound = getPet.responses.find((r) => r.statusCode === 404)!;
|
|
107
|
+
expect(notFound.description).toBe("Pet not found");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("populates security field for protected endpoints", async () => {
|
|
111
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
112
|
+
const endpoints = extractEndpoints(doc);
|
|
113
|
+
|
|
114
|
+
const listPets = endpoints.find((e) => e.operationId === "listPets")!;
|
|
115
|
+
expect(listPets.security).toEqual(["bearerAuth"]);
|
|
116
|
+
|
|
117
|
+
const createPet = endpoints.find((e) => e.operationId === "createPet")!;
|
|
118
|
+
expect(createPet.security).toEqual(["bearerAuth"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("endpoints without security have empty security array", async () => {
|
|
122
|
+
const doc = await readOpenApiSpec(FIXTURE);
|
|
123
|
+
const endpoints = extractEndpoints(doc);
|
|
124
|
+
|
|
125
|
+
const login = endpoints.find((e) => e.operationId === "login")!;
|
|
126
|
+
expect(login.security).toEqual([]);
|
|
127
|
+
|
|
128
|
+
const health = endpoints.find((e) => e.operationId === "healthCheck")!;
|
|
129
|
+
expect(health.security).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|
3
|
+
import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../src/core/generator/openapi-reader.ts";
|
|
4
|
+
import { generateSuites, writeSuites } from "../../src/core/generator/skeleton.ts";
|
|
5
|
+
import { parseFile } from "../../src/core/parser/yaml-parser.ts";
|
|
6
|
+
import { runSuite } from "../../src/core/runner/executor.ts";
|
|
7
|
+
import { tmpdir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { rm, writeFile, mkdir } from "fs/promises";
|
|
10
|
+
|
|
11
|
+
// Save real fetch before any mocks
|
|
12
|
+
const realFetch = globalThis.fetch;
|
|
13
|
+
|
|
14
|
+
// ── Inline mini-server ──────────────────────────
|
|
15
|
+
|
|
16
|
+
function createPetServer() {
|
|
17
|
+
const app = new OpenAPIHono();
|
|
18
|
+
const pets: { id: number; name: string }[] = [];
|
|
19
|
+
let nextId = 1;
|
|
20
|
+
|
|
21
|
+
// Security scheme
|
|
22
|
+
const bearerAuth = app.openAPIRegistry.registerComponent("securitySchemes", "bearerAuth", {
|
|
23
|
+
type: "http",
|
|
24
|
+
scheme: "bearer",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// POST /auth/login — no security
|
|
28
|
+
const loginRoute = createRoute({
|
|
29
|
+
method: "post",
|
|
30
|
+
path: "/auth/login",
|
|
31
|
+
tags: ["Auth"],
|
|
32
|
+
summary: "Login",
|
|
33
|
+
request: {
|
|
34
|
+
body: {
|
|
35
|
+
content: {
|
|
36
|
+
"application/json": {
|
|
37
|
+
schema: z.object({
|
|
38
|
+
username: z.string(),
|
|
39
|
+
password: z.string(),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
responses: {
|
|
46
|
+
200: {
|
|
47
|
+
content: {
|
|
48
|
+
"application/json": {
|
|
49
|
+
schema: z.object({ token: z.string() }),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
description: "Token",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.openapi(loginRoute, (c) => {
|
|
58
|
+
return c.json({ token: "test-token-123" }, 200);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// GET /pets — secured
|
|
62
|
+
const listPetsRoute = createRoute({
|
|
63
|
+
method: "get",
|
|
64
|
+
path: "/pets",
|
|
65
|
+
tags: ["Pets"],
|
|
66
|
+
summary: "List all pets",
|
|
67
|
+
security: [{ bearerAuth: [] }],
|
|
68
|
+
responses: {
|
|
69
|
+
200: {
|
|
70
|
+
content: {
|
|
71
|
+
"application/json": {
|
|
72
|
+
schema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
description: "Pet list",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
app.openapi(listPetsRoute, (c) => {
|
|
81
|
+
return c.json(pets, 200);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// POST /pets — secured
|
|
85
|
+
const createPetRoute = createRoute({
|
|
86
|
+
method: "post",
|
|
87
|
+
path: "/pets",
|
|
88
|
+
tags: ["Pets"],
|
|
89
|
+
summary: "Create a pet",
|
|
90
|
+
security: [{ bearerAuth: [] }],
|
|
91
|
+
request: {
|
|
92
|
+
body: {
|
|
93
|
+
content: {
|
|
94
|
+
"application/json": {
|
|
95
|
+
schema: z.object({ name: z.string() }),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
responses: {
|
|
101
|
+
201: {
|
|
102
|
+
content: {
|
|
103
|
+
"application/json": {
|
|
104
|
+
schema: z.object({ id: z.number(), name: z.string() }),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
description: "Pet created",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
app.openapi(createPetRoute, (c) => {
|
|
113
|
+
const { name } = c.req.valid("json");
|
|
114
|
+
const pet = { id: nextId++, name };
|
|
115
|
+
pets.push(pet);
|
|
116
|
+
return c.json(pet, 201);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// OpenAPI spec endpoint
|
|
120
|
+
app.doc("/doc", {
|
|
121
|
+
openapi: "3.0.0",
|
|
122
|
+
info: { title: "Pet API", version: "1.0.0" },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return app;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Tests ───────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe("Auth flow integration", () => {
|
|
131
|
+
let server: ReturnType<typeof Bun.serve>;
|
|
132
|
+
let TEST_BASE: string;
|
|
133
|
+
const tmpDir = join(tmpdir(), `apitool-auth-integration-${Date.now()}`);
|
|
134
|
+
|
|
135
|
+
beforeAll(async () => {
|
|
136
|
+
globalThis.fetch = realFetch;
|
|
137
|
+
await mkdir(tmpDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
const app = createPetServer();
|
|
140
|
+
server = Bun.serve({
|
|
141
|
+
fetch: app.fetch,
|
|
142
|
+
port: 0,
|
|
143
|
+
});
|
|
144
|
+
TEST_BASE = `http://localhost:${server.port}`;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterAll(async () => {
|
|
148
|
+
server?.stop();
|
|
149
|
+
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("fetch OpenAPI spec from inline server", async () => {
|
|
153
|
+
const res = await fetch(`${TEST_BASE}/doc`);
|
|
154
|
+
expect(res.ok).toBe(true);
|
|
155
|
+
const spec = await res.json() as any;
|
|
156
|
+
expect(spec.openapi).toBe("3.0.0");
|
|
157
|
+
expect(spec.components.securitySchemes.bearerAuth).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("generate auth-aware tests from live spec, then run them", async () => {
|
|
161
|
+
// Fetch and save spec
|
|
162
|
+
const res = await fetch(`${TEST_BASE}/doc`);
|
|
163
|
+
const spec = await res.json();
|
|
164
|
+
const specPath = join(tmpDir, "spec.json");
|
|
165
|
+
await writeFile(specPath, JSON.stringify(spec));
|
|
166
|
+
|
|
167
|
+
// Generate skeleton
|
|
168
|
+
const doc = await readOpenApiSpec(specPath);
|
|
169
|
+
const endpoints = extractEndpoints(doc);
|
|
170
|
+
const securitySchemes = extractSecuritySchemes(doc);
|
|
171
|
+
const suites = generateSuites(endpoints, TEST_BASE, securitySchemes);
|
|
172
|
+
|
|
173
|
+
const outputDir = join(tmpDir, "generated");
|
|
174
|
+
const { written } = await writeSuites(suites, outputDir);
|
|
175
|
+
|
|
176
|
+
// Find the pets suite (has auth + CRUD)
|
|
177
|
+
const petsFile = written.find((f) => f.includes("pets"))!;
|
|
178
|
+
expect(petsFile).toBeDefined();
|
|
179
|
+
|
|
180
|
+
// Write env file with auth credentials
|
|
181
|
+
const envPath = join(outputDir, ".env.yaml");
|
|
182
|
+
await writeFile(envPath, "auth_username: admin\nauth_password: admin\n");
|
|
183
|
+
|
|
184
|
+
// Parse and run the generated tests
|
|
185
|
+
const suite = await parseFile(petsFile);
|
|
186
|
+
|
|
187
|
+
// Login step should be generated first
|
|
188
|
+
expect(suite.tests[0]!.name).toBe("Auth: Login");
|
|
189
|
+
|
|
190
|
+
// Suite headers should include Bearer auth
|
|
191
|
+
expect(suite.headers?.Authorization).toBe("Bearer {{auth_token}}");
|
|
192
|
+
|
|
193
|
+
// Load env vars
|
|
194
|
+
const envText = await Bun.file(envPath).text();
|
|
195
|
+
const env = Bun.YAML.parse(envText) as Record<string, string>;
|
|
196
|
+
|
|
197
|
+
const result = await runSuite(suite, env);
|
|
198
|
+
|
|
199
|
+
// Login should pass and capture token
|
|
200
|
+
const loginResult = result.steps[0]!;
|
|
201
|
+
expect(loginResult.status).toBe("pass");
|
|
202
|
+
expect(loginResult.captures.auth_token).toBeDefined();
|
|
203
|
+
expect(typeof loginResult.captures.auth_token).toBe("string");
|
|
204
|
+
|
|
205
|
+
// Create pet should pass (uses captured token)
|
|
206
|
+
const createResult = result.steps.find((s) => s.name === "Create a pet");
|
|
207
|
+
expect(createResult?.status).toBe("pass");
|
|
208
|
+
expect(createResult?.response?.status).toBe(201);
|
|
209
|
+
|
|
210
|
+
// List pets should pass
|
|
211
|
+
const listResult = result.steps.find((s) => s.name === "List all pets");
|
|
212
|
+
expect(listResult?.status).toBe("pass");
|
|
213
|
+
expect(listResult?.response?.status).toBe(200);
|
|
214
|
+
|
|
215
|
+
expect(result.passed).toBeGreaterThanOrEqual(3);
|
|
216
|
+
}, 30000);
|
|
217
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("../../src/core/generator/openapi-reader.ts", () => ({
|
|
4
|
+
readOpenApiSpec: mock(() => Promise.resolve({ info: { title: "API" }, paths: {} })),
|
|
5
|
+
extractEndpoints: mock(() => [
|
|
6
|
+
{ method: "GET", path: "/users", summary: "List users", tags: ["users"], parameters: [], responses: [] },
|
|
7
|
+
{ method: "POST", path: "/users", summary: "Create user", tags: ["users"], parameters: [], responses: [] },
|
|
8
|
+
]),
|
|
9
|
+
extractSecuritySchemes: mock(() => []),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
mock.module("../../src/core/generator/coverage-scanner.ts", () => ({
|
|
13
|
+
scanCoveredEndpoints: mock(() => Promise.resolve([
|
|
14
|
+
{ method: "GET", path: "/users", file: "tests/users.yaml" },
|
|
15
|
+
])),
|
|
16
|
+
filterUncoveredEndpoints: mock((all: any[], _covered: any[]) => all.slice(1)),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
afterAll(() => { mock.restore(); });
|
|
20
|
+
|
|
21
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
|
+
import { registerCoverageAnalysisTool } from "../../src/mcp/tools/coverage-analysis.ts";
|
|
23
|
+
import { readOpenApiSpec, extractEndpoints } from "../../src/core/generator/openapi-reader.ts";
|
|
24
|
+
|
|
25
|
+
describe("MCP coverage_analysis", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
(readOpenApiSpec as ReturnType<typeof mock>).mockClear();
|
|
28
|
+
(extractEndpoints as ReturnType<typeof mock>).mockClear();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("returns coverage analysis", async () => {
|
|
32
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
33
|
+
registerCoverageAnalysisTool(server);
|
|
34
|
+
const tool = (server as any)._registeredTools["coverage_analysis"];
|
|
35
|
+
|
|
36
|
+
const result = await tool.handler({ specPath: "spec.yaml", testsDir: "./tests" });
|
|
37
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
38
|
+
expect(parsed.totalEndpoints).toBe(2);
|
|
39
|
+
expect(parsed.covered).toBe(1);
|
|
40
|
+
expect(parsed.uncovered).toBe(1);
|
|
41
|
+
expect(parsed.percentage).toBe(50);
|
|
42
|
+
expect(parsed.uncoveredEndpoints).toHaveLength(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("returns error when spec has no endpoints", async () => {
|
|
46
|
+
(extractEndpoints as ReturnType<typeof mock>).mockReturnValueOnce([]);
|
|
47
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
48
|
+
registerCoverageAnalysisTool(server);
|
|
49
|
+
const tool = (server as any)._registeredTools["coverage_analysis"];
|
|
50
|
+
|
|
51
|
+
const result = await tool.handler({ specPath: "empty.yaml", testsDir: "./tests" });
|
|
52
|
+
expect(result.isError).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("returns error on spec read failure", async () => {
|
|
56
|
+
(readOpenApiSpec as ReturnType<typeof mock>).mockRejectedValueOnce(new Error("File not found"));
|
|
57
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
58
|
+
registerCoverageAnalysisTool(server);
|
|
59
|
+
const tool = (server as any)._registeredTools["coverage_analysis"];
|
|
60
|
+
|
|
61
|
+
const result = await tool.handler({ specPath: "bad.yaml", testsDir: "./tests" });
|
|
62
|
+
expect(result.isError).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, test, expect, mock, afterAll } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("../../src/core/generator/openapi-reader.ts", () => ({
|
|
4
|
+
readOpenApiSpec: mock(() => Promise.resolve({
|
|
5
|
+
info: { title: "Pet Store", version: "1.0.0" },
|
|
6
|
+
servers: [{ url: "https://petstore.io/v1" }],
|
|
7
|
+
paths: {},
|
|
8
|
+
})),
|
|
9
|
+
extractEndpoints: mock(() => [
|
|
10
|
+
{
|
|
11
|
+
method: "GET",
|
|
12
|
+
path: "/pets",
|
|
13
|
+
summary: "List pets",
|
|
14
|
+
tags: ["pets"],
|
|
15
|
+
parameters: [{ name: "limit", in: "query", required: false, schema: { type: "integer" } }],
|
|
16
|
+
responses: [
|
|
17
|
+
{ statusCode: 200, description: "OK", schema: { type: "array", items: { type: "object", properties: { id: { type: "integer" }, name: { type: "string" } } } } },
|
|
18
|
+
],
|
|
19
|
+
requestBodySchema: undefined,
|
|
20
|
+
requestBodyContentType: undefined,
|
|
21
|
+
security: [],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
method: "POST",
|
|
25
|
+
path: "/pets",
|
|
26
|
+
summary: "Create pet",
|
|
27
|
+
tags: ["pets"],
|
|
28
|
+
parameters: [],
|
|
29
|
+
responses: [
|
|
30
|
+
{ statusCode: 201, description: "Created", schema: { type: "object", required: ["id"], properties: { id: { type: "integer" }, name: { type: "string" } } } },
|
|
31
|
+
{ statusCode: 400, description: "Validation error" },
|
|
32
|
+
],
|
|
33
|
+
requestBodySchema: { type: "object", required: ["name"], properties: { name: { type: "string" }, species: { type: "string" } } },
|
|
34
|
+
requestBodyContentType: "application/json",
|
|
35
|
+
security: ["bearerAuth"],
|
|
36
|
+
},
|
|
37
|
+
]),
|
|
38
|
+
extractSecuritySchemes: mock(() => [
|
|
39
|
+
{ name: "bearerAuth", type: "http", scheme: "bearer" },
|
|
40
|
+
]),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
afterAll(() => { mock.restore(); });
|
|
44
|
+
|
|
45
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
46
|
+
import { registerExploreApiTool } from "../../src/mcp/tools/explore-api.ts";
|
|
47
|
+
|
|
48
|
+
describe("MCP explore_api with includeSchemas", () => {
|
|
49
|
+
test("without includeSchemas — no schemas in output", async () => {
|
|
50
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
51
|
+
registerExploreApiTool(server);
|
|
52
|
+
|
|
53
|
+
const tool = (server as any)._registeredTools["explore_api"];
|
|
54
|
+
const result = await tool.handler({ specPath: "petstore.yaml" });
|
|
55
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
56
|
+
|
|
57
|
+
const postEndpoint = parsed.endpoints.find((e: any) => e.method === "POST");
|
|
58
|
+
expect(postEndpoint.hasRequestBody).toBe(true);
|
|
59
|
+
expect(postEndpoint.requestBodySchema).toBeUndefined();
|
|
60
|
+
expect(postEndpoint.responses[0].schema).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("with includeSchemas=true — includes compressed schemas", async () => {
|
|
64
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
65
|
+
registerExploreApiTool(server);
|
|
66
|
+
|
|
67
|
+
const tool = (server as any)._registeredTools["explore_api"];
|
|
68
|
+
const result = await tool.handler({ specPath: "petstore.yaml", includeSchemas: true });
|
|
69
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
70
|
+
|
|
71
|
+
const postEndpoint = parsed.endpoints.find((e: any) => e.method === "POST");
|
|
72
|
+
|
|
73
|
+
// Should have request body schema
|
|
74
|
+
expect(postEndpoint.requestBodySchema).toBe("{ name: string (req), species: string }");
|
|
75
|
+
expect(postEndpoint.requestBodyContentType).toBe("application/json");
|
|
76
|
+
|
|
77
|
+
// Should have response schemas
|
|
78
|
+
expect(postEndpoint.responses[0].schema).toBe("{ id: integer (req), name: string }");
|
|
79
|
+
// 400 has no schema
|
|
80
|
+
expect(postEndpoint.responses[1].schema).toBeUndefined();
|
|
81
|
+
|
|
82
|
+
// Should have security
|
|
83
|
+
expect(postEndpoint.security).toEqual(["bearerAuth"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("with includeSchemas=true — GET endpoint has response schema", async () => {
|
|
87
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
88
|
+
registerExploreApiTool(server);
|
|
89
|
+
|
|
90
|
+
const tool = (server as any)._registeredTools["explore_api"];
|
|
91
|
+
const result = await tool.handler({ specPath: "petstore.yaml", includeSchemas: true });
|
|
92
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
93
|
+
|
|
94
|
+
const getEndpoint = parsed.endpoints.find((e: any) => e.method === "GET");
|
|
95
|
+
|
|
96
|
+
// Should have response schema for 200
|
|
97
|
+
expect(getEndpoint.responses[0].schema).toBe("[{ id: integer, name: string }]");
|
|
98
|
+
|
|
99
|
+
// GET has no request body
|
|
100
|
+
expect(getEndpoint.requestBodySchema).toBeUndefined();
|
|
101
|
+
|
|
102
|
+
// Parameter should have type info
|
|
103
|
+
expect(getEndpoint.parameters[0].type).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
});
|