import { describe, it, expect, beforeEach, beforeAll } from "vitest";
import { openapiLoad, openapiListEndpoints, openapiGetEndpoint } from "../src/tools.js";
import { store } from "../src/openapi-store.js";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import type { LoadResult, ListEndpointsResult, EndpointDetail, ErrorResponse } from "../src/types.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const PETSTORE_YAML = join(__dirname, "fixtures", "petstore.yaml");
function isError(result: unknown): result is ErrorResponse {
return typeof result === "object" && result !== null && "error" in result && (result as any).error === true;
}
describe("openapi_load", () => {
beforeEach(() => {
store.clear();
});
it("should successfully load a valid spec", async () => {
const result = await openapiLoad({ source: PETSTORE_YAML, alias: "petstore" });
expect(isError(result)).toBe(false);
const loadResult = result as LoadResult;
expect(loadResult.success).toBe(true);
expect(loadResult.alias).toBe("petstore");
expect(loadResult.title).toBe("Swagger Petstore - OpenAPI 3.0");
expect(loadResult.version).toBe("1.0.27");
expect(loadResult.endpointCount).toBeGreaterThan(0);
expect(loadResult.tags).toContain("pet");
});
it("should return error for invalid URL", async () => {
const result = await openapiLoad({ source: "/non/existent/file.yaml", alias: "test" });
expect(isError(result)).toBe(true);
const errorResult = result as ErrorResponse;
expect(errorResult.code).toBe("FETCH_ERROR");
});
it("should return error for invalid spec content", async () => {
const invalidPath = join(__dirname, "fixtures", "invalid-tool.yaml");
const fs = await import("fs/promises");
await fs.writeFile(invalidPath, "invalid: yaml: content: here:");
const result = await openapiLoad({ source: invalidPath, alias: "invalid" });
expect(isError(result)).toBe(true);
const errorResult = result as ErrorResponse;
expect(errorResult.code).toBe("PARSE_ERROR");
await fs.unlink(invalidPath);
});
it("should reload/replace existing spec with same alias", async () => {
await openapiLoad({ source: PETSTORE_YAML, alias: "petstore" });
const result = await openapiLoad({ source: PETSTORE_YAML, alias: "petstore" });
expect(isError(result)).toBe(false);
expect((result as LoadResult).success).toBe(true);
expect(store.list()).toEqual(["petstore"]);
});
it("should return validation error for missing source", async () => {
const result = await openapiLoad({ source: "", alias: "test" });
expect(isError(result)).toBe(true);
expect((result as ErrorResponse).code).toBe("VALIDATION_ERROR");
});
it("should return validation error for missing alias", async () => {
const result = await openapiLoad({ source: PETSTORE_YAML, alias: "" });
expect(isError(result)).toBe(true);
expect((result as ErrorResponse).code).toBe("VALIDATION_ERROR");
});
});
describe("openapi_list_endpoints", () => {
beforeAll(async () => {
store.clear();
await openapiLoad({ source: PETSTORE_YAML, alias: "petstore" });
});
it("should list all endpoints", () => {
const result = openapiListEndpoints({ alias: "petstore" });
expect(isError(result)).toBe(false);
const listResult = result as ListEndpointsResult;
expect(listResult.alias).toBe("petstore");
expect(listResult.total).toBeGreaterThan(0);
expect(listResult.endpoints.length).toBe(listResult.total);
// Check endpoint structure
const endpoint = listResult.endpoints[0];
expect(endpoint).toHaveProperty("method");
expect(endpoint).toHaveProperty("path");
expect(endpoint).toHaveProperty("operationId");
expect(endpoint).toHaveProperty("summary");
expect(endpoint).toHaveProperty("tags");
expect(endpoint).toHaveProperty("deprecated");
});
it("should filter by tag", () => {
const result = openapiListEndpoints({ alias: "petstore", tag: "pet" });
expect(isError(result)).toBe(false);
const listResult = result as ListEndpointsResult;
// All endpoints should have the 'pet' tag
listResult.endpoints.forEach((ep) => {
expect(ep.tags).toContain("pet");
});
});
it("should filter by search term", () => {
const result = openapiListEndpoints({ alias: "petstore", search: "pet" });
expect(isError(result)).toBe(false);
const listResult = result as ListEndpointsResult;
expect(listResult.total).toBeGreaterThan(0);
// All endpoints should match the search term in path, summary, or operationId
listResult.endpoints.forEach((ep) => {
const matchesPath = ep.path.toLowerCase().includes("pet");
const matchesSummary = ep.summary?.toLowerCase().includes("pet") ?? false;
const matchesOpId = ep.operationId?.toLowerCase().includes("pet") ?? false;
expect(matchesPath || matchesSummary || matchesOpId).toBe(true);
});
});
it("should filter by both tag and search (AND logic)", () => {
const result = openapiListEndpoints({ alias: "petstore", tag: "store", search: "order" });
expect(isError(result)).toBe(false);
const listResult = result as ListEndpointsResult;
listResult.endpoints.forEach((ep) => {
expect(ep.tags).toContain("store");
const matchesSearch =
ep.path.toLowerCase().includes("order") ||
(ep.summary?.toLowerCase().includes("order") ?? false) ||
(ep.operationId?.toLowerCase().includes("order") ?? false);
expect(matchesSearch).toBe(true);
});
});
it("should return error for unknown alias", () => {
const result = openapiListEndpoints({ alias: "unknown" });
expect(isError(result)).toBe(true);
const errorResult = result as ErrorResponse;
expect(errorResult.code).toBe("SPEC_NOT_FOUND");
expect(errorResult.message).toContain("petstore"); // Should suggest available specs
});
it("should sort results by path then method", () => {
const result = openapiListEndpoints({ alias: "petstore" });
expect(isError(result)).toBe(false);
const listResult = result as ListEndpointsResult;
// Check sorting
for (let i = 1; i < listResult.endpoints.length; i++) {
const prev = listResult.endpoints[i - 1];
const curr = listResult.endpoints[i];
if (prev.path === curr.path) {
expect(prev.method.localeCompare(curr.method)).toBeLessThanOrEqual(0);
} else {
expect(prev.path.localeCompare(curr.path)).toBeLessThan(0);
}
}
});
});
describe("openapi_get_endpoint", () => {
beforeAll(async () => {
store.clear();
await openapiLoad({ source: PETSTORE_YAML, alias: "petstore" });
});
it("should get endpoint with path parameters", () => {
const result = openapiGetEndpoint({
alias: "petstore",
method: "GET",
path: "/pet/{petId}"
});
expect(isError(result)).toBe(false);
const endpoint = result as EndpointDetail;
expect(endpoint.method).toBe("GET");
expect(endpoint.path).toBe("/pet/{petId}");
expect(endpoint.operationId).toBe("getPetById");
// Check path parameter
const petIdParam = endpoint.parameters.find(p => p.name === "petId");
expect(petIdParam).toBeDefined();
expect(petIdParam?.in).toBe("path");
expect(petIdParam?.required).toBe(true);
});
it("should get endpoint with query parameters", () => {
const result = openapiGetEndpoint({
alias: "petstore",
method: "GET",
path: "/pet/findByStatus"
});
expect(isError(result)).toBe(false);
const endpoint = result as EndpointDetail;
const statusParam = endpoint.parameters.find(p => p.name === "status");
expect(statusParam).toBeDefined();
expect(statusParam?.in).toBe("query");
});
it("should get endpoint with request body", () => {
const result = openapiGetEndpoint({
alias: "petstore",
method: "POST",
path: "/pet"
});
expect(isError(result)).toBe(false);
const endpoint = result as EndpointDetail;
expect(endpoint.requestBody).not.toBeNull();
expect(endpoint.requestBody?.required).toBe(true);
expect(endpoint.requestBody?.content).toHaveProperty("application/json");
expect(endpoint.requestBody?.content["application/json"].schema).toBeDefined();
});
it("should get endpoint with multiple response codes", () => {
const result = openapiGetEndpoint({
alias: "petstore",
method: "GET",
path: "/pet/{petId}"
});
expect(isError(result)).toBe(false);
const endpoint = result as EndpointDetail;
expect(endpoint.responses).toHaveProperty("200");
expect(endpoint.responses).toHaveProperty("400");
expect(endpoint.responses).toHaveProperty("404");
expect(endpoint.responses["200"].description).toBeDefined();
});
it("should support case-insensitive method matching", () => {
const resultLower = openapiGetEndpoint({
alias: "petstore",
method: "get",
path: "/pet/{petId}"
});
const resultUpper = openapiGetEndpoint({
alias: "petstore",
method: "GET",
path: "/pet/{petId}"
});
const resultMixed = openapiGetEndpoint({
alias: "petstore",
method: "Get",
path: "/pet/{petId}"
});
expect(isError(resultLower)).toBe(false);
expect(isError(resultUpper)).toBe(false);
expect(isError(resultMixed)).toBe(false);
// All should return uppercase method
expect((resultLower as EndpointDetail).method).toBe("GET");
expect((resultUpper as EndpointDetail).method).toBe("GET");
expect((resultMixed as EndpointDetail).method).toBe("GET");
});
it("should return error for non-existent endpoint", () => {
const result = openapiGetEndpoint({
alias: "petstore",
method: "GET",
path: "/nonexistent"
});
expect(isError(result)).toBe(true);
const errorResult = result as ErrorResponse;
expect(errorResult.code).toBe("ENDPOINT_NOT_FOUND");
});
it("should return error for wrong method on existing path", () => {
const result = openapiGetEndpoint({
alias: "petstore",
method: "PATCH",
path: "/pet/{petId}"
});
expect(isError(result)).toBe(true);
const errorResult = result as ErrorResponse;
expect(errorResult.code).toBe("ENDPOINT_NOT_FOUND");
expect(errorResult.message).toContain("Available methods");
});
it("should return error for unknown alias", () => {
const result = openapiGetEndpoint({
alias: "unknown",
method: "GET",
path: "/pet/{petId}"
});
expect(isError(result)).toBe(true);
const errorResult = result as ErrorResponse;
expect(errorResult.code).toBe("SPEC_NOT_FOUND");
});
it("should include resolved schemas without $ref", () => {
const result = openapiGetEndpoint({
alias: "petstore",
method: "POST",
path: "/pet"
});
expect(isError(result)).toBe(false);
const endpoint = result as EndpointDetail;
const jsonSchema = endpoint.requestBody?.content["application/json"].schema;
expect(jsonSchema).toBeDefined();
// Schema should be resolved (no $ref at top level)
expect(jsonSchema).not.toHaveProperty("$ref");
expect(jsonSchema).toHaveProperty("type");
});
it("should include all endpoint metadata", () => {
const result = openapiGetEndpoint({
alias: "petstore",
method: "PUT",
path: "/pet"
});
expect(isError(result)).toBe(false);
const endpoint = result as EndpointDetail;
expect(endpoint.method).toBe("PUT");
expect(endpoint.path).toBe("/pet");
expect(endpoint.operationId).toBe("updatePet");
expect(endpoint.summary).toBeDefined();
expect(endpoint.description).toBeDefined();
expect(endpoint.tags).toContain("pet");
expect(typeof endpoint.deprecated).toBe("boolean");
expect(Array.isArray(endpoint.parameters)).toBe(true);
expect(typeof endpoint.responses).toBe("object");
});
});