@schmock/openapi 1.1.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/dist/crud-detector.d.ts +35 -0
- package/dist/crud-detector.d.ts.map +1 -0
- package/dist/crud-detector.js +153 -0
- package/dist/generators.d.ts +14 -0
- package/dist/generators.d.ts.map +1 -0
- package/dist/generators.js +158 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +221 -0
- package/dist/normalizer.d.ts +14 -0
- package/dist/normalizer.d.ts.map +1 -0
- package/dist/normalizer.js +194 -0
- package/dist/parser.d.ts +32 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +282 -0
- package/dist/plugin.d.ts +32 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +129 -0
- package/dist/seed.d.ts +15 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +41 -0
- package/package.json +45 -0
- package/src/__fixtures__/faker-stress-test.openapi.yaml +1030 -0
- package/src/__fixtures__/openapi31.json +34 -0
- package/src/__fixtures__/petstore-openapi3.json +168 -0
- package/src/__fixtures__/petstore-swagger2.json +141 -0
- package/src/__fixtures__/scalar-galaxy.yaml +1314 -0
- package/src/__fixtures__/stripe-fixtures3.json +6542 -0
- package/src/__fixtures__/stripe-spec3.yaml +161621 -0
- package/src/__fixtures__/train-travel.yaml +1264 -0
- package/src/crud-detector.test.ts +150 -0
- package/src/crud-detector.ts +194 -0
- package/src/generators.test.ts +214 -0
- package/src/generators.ts +212 -0
- package/src/index.ts +4 -0
- package/src/normalizer.test.ts +253 -0
- package/src/normalizer.ts +233 -0
- package/src/parser.test.ts +181 -0
- package/src/parser.ts +389 -0
- package/src/plugin.test.ts +205 -0
- package/src/plugin.ts +185 -0
- package/src/seed.ts +62 -0
- package/src/steps/openapi-crud.steps.ts +132 -0
- package/src/steps/openapi-parsing.steps.ts +111 -0
- package/src/steps/openapi-seed.steps.ts +94 -0
- package/src/stress.test.ts +2814 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { detectCrudResources } from "./crud-detector";
|
|
3
|
+
import type { ParsedPath } from "./parser";
|
|
4
|
+
|
|
5
|
+
function makePath(
|
|
6
|
+
method: Schmock.HttpMethod,
|
|
7
|
+
path: string,
|
|
8
|
+
responseSchema?: Record<string, unknown>,
|
|
9
|
+
): ParsedPath {
|
|
10
|
+
const responses = new Map<number, { schema?: any; description: string }>();
|
|
11
|
+
if (responseSchema) {
|
|
12
|
+
responses.set(200, { schema: responseSchema, description: "OK" });
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
path,
|
|
16
|
+
method,
|
|
17
|
+
parameters: [],
|
|
18
|
+
responses,
|
|
19
|
+
tags: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("detectCrudResources", () => {
|
|
24
|
+
it("detects a standard CRUD resource", () => {
|
|
25
|
+
const paths: ParsedPath[] = [
|
|
26
|
+
makePath("GET", "/pets", {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: { type: "object", properties: { petId: { type: "integer" } } },
|
|
29
|
+
}),
|
|
30
|
+
makePath("POST", "/pets"),
|
|
31
|
+
makePath("GET", "/pets/:petId", {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { petId: { type: "integer" } },
|
|
34
|
+
}),
|
|
35
|
+
makePath("PUT", "/pets/:petId"),
|
|
36
|
+
makePath("DELETE", "/pets/:petId"),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const result = detectCrudResources(paths);
|
|
40
|
+
|
|
41
|
+
expect(result.resources).toHaveLength(1);
|
|
42
|
+
expect(result.nonCrudPaths).toHaveLength(0);
|
|
43
|
+
|
|
44
|
+
const resource = result.resources[0];
|
|
45
|
+
expect(resource.name).toBe("pets");
|
|
46
|
+
expect(resource.basePath).toBe("/pets");
|
|
47
|
+
expect(resource.itemPath).toBe("/pets/:petId");
|
|
48
|
+
expect(resource.idParam).toBe("petId");
|
|
49
|
+
expect(resource.operations).toContain("list");
|
|
50
|
+
expect(resource.operations).toContain("create");
|
|
51
|
+
expect(resource.operations).toContain("read");
|
|
52
|
+
expect(resource.operations).toContain("update");
|
|
53
|
+
expect(resource.operations).toContain("delete");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("detects a read-only API", () => {
|
|
57
|
+
const paths: ParsedPath[] = [
|
|
58
|
+
makePath("GET", "/articles", {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: { articleId: { type: "integer" } },
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
makePath("GET", "/articles/:articleId"),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const result = detectCrudResources(paths);
|
|
69
|
+
|
|
70
|
+
expect(result.resources).toHaveLength(1);
|
|
71
|
+
expect(result.resources[0].operations).toEqual(["list", "read"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("handles non-CRUD endpoints", () => {
|
|
75
|
+
const paths: ParsedPath[] = [
|
|
76
|
+
makePath("GET", "/health"),
|
|
77
|
+
makePath("POST", "/login"),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const result = detectCrudResources(paths);
|
|
81
|
+
|
|
82
|
+
expect(result.resources).toHaveLength(0);
|
|
83
|
+
expect(result.nonCrudPaths).toHaveLength(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("handles mixed CRUD and non-CRUD endpoints", () => {
|
|
87
|
+
const paths: ParsedPath[] = [
|
|
88
|
+
makePath("GET", "/pets"),
|
|
89
|
+
makePath("POST", "/pets"),
|
|
90
|
+
makePath("GET", "/health"),
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const result = detectCrudResources(paths);
|
|
94
|
+
|
|
95
|
+
expect(result.resources).toHaveLength(1);
|
|
96
|
+
expect(result.resources[0].name).toBe("pets");
|
|
97
|
+
expect(result.nonCrudPaths).toHaveLength(1);
|
|
98
|
+
expect(result.nonCrudPaths[0].path).toBe("/health");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("handles nested resources", () => {
|
|
102
|
+
const paths: ParsedPath[] = [
|
|
103
|
+
makePath("GET", "/owners/:ownerId/pets"),
|
|
104
|
+
makePath("POST", "/owners/:ownerId/pets"),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const result = detectCrudResources(paths);
|
|
108
|
+
|
|
109
|
+
expect(result.resources).toHaveLength(1);
|
|
110
|
+
expect(result.resources[0].name).toBe("pets");
|
|
111
|
+
expect(result.resources[0].basePath).toBe("/owners/:ownerId/pets");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("extracts schema from list response items", () => {
|
|
115
|
+
const itemSchema = {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
petId: { type: "integer" },
|
|
119
|
+
name: { type: "string" },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const paths: ParsedPath[] = [
|
|
124
|
+
makePath("GET", "/pets", {
|
|
125
|
+
type: "array",
|
|
126
|
+
items: itemSchema,
|
|
127
|
+
}),
|
|
128
|
+
makePath("POST", "/pets"),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const result = detectCrudResources(paths);
|
|
132
|
+
|
|
133
|
+
expect(result.resources).toHaveLength(1);
|
|
134
|
+
expect(result.resources[0].schema).toEqual(itemSchema);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not duplicate update operation for PUT and PATCH", () => {
|
|
138
|
+
const paths: ParsedPath[] = [
|
|
139
|
+
makePath("GET", "/pets"),
|
|
140
|
+
makePath("PUT", "/pets/:petId"),
|
|
141
|
+
makePath("PATCH", "/pets/:petId"),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const result = detectCrudResources(paths);
|
|
145
|
+
const updateCount = result.resources[0].operations.filter(
|
|
146
|
+
(op) => op === "update",
|
|
147
|
+
).length;
|
|
148
|
+
expect(updateCount).toBe(1);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/// <reference path="../../../types/schmock.d.ts" />
|
|
2
|
+
|
|
3
|
+
import type { JSONSchema7 } from "json-schema";
|
|
4
|
+
import type { ParsedPath } from "./parser.js";
|
|
5
|
+
|
|
6
|
+
export type CrudOperation = "list" | "create" | "read" | "update" | "delete";
|
|
7
|
+
|
|
8
|
+
export interface CrudResource {
|
|
9
|
+
/** Resource name e.g. "pets" */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Collection path e.g. "/pets" */
|
|
12
|
+
basePath: string;
|
|
13
|
+
/** Item path e.g. "/pets/:petId" */
|
|
14
|
+
itemPath: string;
|
|
15
|
+
/** ID parameter name e.g. "petId" */
|
|
16
|
+
idParam: string;
|
|
17
|
+
/** Detected CRUD operations */
|
|
18
|
+
operations: CrudOperation[];
|
|
19
|
+
/** Response schema for the resource item */
|
|
20
|
+
schema?: JSONSchema7;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DetectionResult {
|
|
24
|
+
resources: CrudResource[];
|
|
25
|
+
/** Paths that didn't match any CRUD pattern */
|
|
26
|
+
nonCrudPaths: ParsedPath[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect CRUD resource patterns from parsed OpenAPI paths.
|
|
31
|
+
*
|
|
32
|
+
* Patterns:
|
|
33
|
+
* - GET /resources → list
|
|
34
|
+
* - POST /resources → create
|
|
35
|
+
* - GET /resources/:id → read
|
|
36
|
+
* - PUT/PATCH /resources/:id → update
|
|
37
|
+
* - DELETE /resources/:id → delete
|
|
38
|
+
*/
|
|
39
|
+
export function detectCrudResources(paths: ParsedPath[]): DetectionResult {
|
|
40
|
+
// Group paths by their base path (strip trailing /:param)
|
|
41
|
+
const groups = new Map<string, ParsedPath[]>();
|
|
42
|
+
const nonCrudPaths: ParsedPath[] = [];
|
|
43
|
+
|
|
44
|
+
for (const p of paths) {
|
|
45
|
+
const basePath = getCollectionPath(p.path);
|
|
46
|
+
if (!basePath) {
|
|
47
|
+
nonCrudPaths.push(p);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const existing = groups.get(basePath) ?? [];
|
|
51
|
+
existing.push(p);
|
|
52
|
+
groups.set(basePath, existing);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resources: CrudResource[] = [];
|
|
56
|
+
|
|
57
|
+
for (const [basePath, groupPaths] of groups) {
|
|
58
|
+
const resource = buildResource(basePath, groupPaths);
|
|
59
|
+
if (resource) {
|
|
60
|
+
resources.push(resource);
|
|
61
|
+
} else {
|
|
62
|
+
// If no CRUD pattern detected, treat as non-CRUD
|
|
63
|
+
nonCrudPaths.push(...groupPaths);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { resources, nonCrudPaths };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract the collection base path from a path.
|
|
72
|
+
* "/pets" → "/pets"
|
|
73
|
+
* "/pets/:petId" → "/pets"
|
|
74
|
+
* "/owners/:ownerId/pets" → "/owners/:ownerId/pets"
|
|
75
|
+
* "/owners/:ownerId/pets/:petId" → "/owners/:ownerId/pets"
|
|
76
|
+
*/
|
|
77
|
+
function getCollectionPath(path: string): string | undefined {
|
|
78
|
+
const segments = path.split("/").filter(Boolean);
|
|
79
|
+
if (segments.length === 0) return undefined;
|
|
80
|
+
|
|
81
|
+
// If last segment is a param (:xyz), remove it to get collection path
|
|
82
|
+
const last = segments[segments.length - 1];
|
|
83
|
+
if (last.startsWith(":")) {
|
|
84
|
+
return `/${segments.slice(0, -1).join("/")}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Otherwise the path itself is a potential collection path
|
|
88
|
+
return `/${segments.join("/")}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildResource(
|
|
92
|
+
basePath: string,
|
|
93
|
+
paths: ParsedPath[],
|
|
94
|
+
): CrudResource | undefined {
|
|
95
|
+
const operations: CrudOperation[] = [];
|
|
96
|
+
let itemPath = "";
|
|
97
|
+
let idParam = "";
|
|
98
|
+
let schema: JSONSchema7 | undefined;
|
|
99
|
+
|
|
100
|
+
for (const p of paths) {
|
|
101
|
+
const isCollection = p.path === basePath;
|
|
102
|
+
const isItem = !isCollection && p.path.startsWith(basePath);
|
|
103
|
+
|
|
104
|
+
if (isCollection) {
|
|
105
|
+
if (p.method === "GET") {
|
|
106
|
+
operations.push("list");
|
|
107
|
+
// Try to extract item schema from list response (array items)
|
|
108
|
+
const listSchema = getSuccessResponseSchema(p);
|
|
109
|
+
if (listSchema && listSchema.type === "array" && listSchema.items) {
|
|
110
|
+
const items = Array.isArray(listSchema.items)
|
|
111
|
+
? listSchema.items[0]
|
|
112
|
+
: listSchema.items;
|
|
113
|
+
if (typeof items === "object" && items !== null) {
|
|
114
|
+
schema = schema ?? (items as JSONSchema7);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else if (p.method === "POST") {
|
|
118
|
+
operations.push("create");
|
|
119
|
+
}
|
|
120
|
+
} else if (isItem) {
|
|
121
|
+
// Extract ID param from the item path
|
|
122
|
+
const paramSegments = p.path
|
|
123
|
+
.slice(basePath.length)
|
|
124
|
+
.split("/")
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
if (paramSegments.length === 1 && paramSegments[0].startsWith(":")) {
|
|
127
|
+
const param = paramSegments[0].slice(1);
|
|
128
|
+
if (!idParam) {
|
|
129
|
+
idParam = param;
|
|
130
|
+
itemPath = p.path;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (p.method === "GET") {
|
|
134
|
+
operations.push("read");
|
|
135
|
+
schema = schema ?? getSuccessResponseSchema(p);
|
|
136
|
+
} else if (p.method === "PUT" || p.method === "PATCH") {
|
|
137
|
+
if (!operations.includes("update")) {
|
|
138
|
+
operations.push("update");
|
|
139
|
+
}
|
|
140
|
+
} else if (p.method === "DELETE") {
|
|
141
|
+
operations.push("delete");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (operations.length === 0) return undefined;
|
|
148
|
+
|
|
149
|
+
// Require evidence of a genuine CRUD collection:
|
|
150
|
+
// either item-level operations (read/update/delete) exist,
|
|
151
|
+
// or both list AND create exist on the collection path.
|
|
152
|
+
// Single GET /health or POST /login don't qualify.
|
|
153
|
+
const hasItemOps = operations.some(
|
|
154
|
+
(op) => op === "read" || op === "update" || op === "delete",
|
|
155
|
+
);
|
|
156
|
+
const hasList = operations.includes("list");
|
|
157
|
+
const hasCreate = operations.includes("create");
|
|
158
|
+
if (!hasItemOps && !(hasList && hasCreate)) return undefined;
|
|
159
|
+
|
|
160
|
+
// If we only have collection operations, infer item path
|
|
161
|
+
if (!itemPath) {
|
|
162
|
+
const resourceName = basePath.split("/").filter(Boolean).pop() ?? "";
|
|
163
|
+
const singular = resourceName.endsWith("s")
|
|
164
|
+
? resourceName.slice(0, -1)
|
|
165
|
+
: resourceName;
|
|
166
|
+
idParam = `${singular}Id`;
|
|
167
|
+
itemPath = `${basePath}/:${idParam}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const name = basePath.split("/").filter(Boolean).pop() ?? basePath;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
name,
|
|
174
|
+
basePath,
|
|
175
|
+
itemPath,
|
|
176
|
+
idParam,
|
|
177
|
+
operations,
|
|
178
|
+
schema,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getSuccessResponseSchema(p: ParsedPath): JSONSchema7 | undefined {
|
|
183
|
+
// Try 200, then 201, then first 2xx
|
|
184
|
+
for (const code of [200, 201]) {
|
|
185
|
+
const resp = p.responses.get(code);
|
|
186
|
+
if (resp?.schema) return resp.schema;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const [code, resp] of p.responses) {
|
|
190
|
+
if (code >= 200 && code < 300 && resp.schema) return resp.schema;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { CrudResource } from "./crud-detector";
|
|
3
|
+
import {
|
|
4
|
+
createCreateGenerator,
|
|
5
|
+
createDeleteGenerator,
|
|
6
|
+
createListGenerator,
|
|
7
|
+
createReadGenerator,
|
|
8
|
+
createUpdateGenerator,
|
|
9
|
+
generateSeedItems,
|
|
10
|
+
} from "./generators";
|
|
11
|
+
|
|
12
|
+
function makeResource(overrides?: Partial<CrudResource>): CrudResource {
|
|
13
|
+
return {
|
|
14
|
+
name: "pets",
|
|
15
|
+
basePath: "/pets",
|
|
16
|
+
itemPath: "/pets/:petId",
|
|
17
|
+
idParam: "petId",
|
|
18
|
+
operations: ["list", "create", "read", "update", "delete"],
|
|
19
|
+
schema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
petId: { type: "integer" },
|
|
23
|
+
name: { type: "string" },
|
|
24
|
+
},
|
|
25
|
+
required: ["petId", "name"],
|
|
26
|
+
},
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeContext(
|
|
32
|
+
overrides?: Partial<Schmock.RequestContext>,
|
|
33
|
+
): Schmock.RequestContext {
|
|
34
|
+
return {
|
|
35
|
+
method: "GET",
|
|
36
|
+
path: "/pets",
|
|
37
|
+
params: {},
|
|
38
|
+
query: {},
|
|
39
|
+
headers: {},
|
|
40
|
+
state: {},
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("generators", () => {
|
|
46
|
+
describe("CRUD lifecycle", () => {
|
|
47
|
+
it("creates, reads, updates, lists, and deletes", () => {
|
|
48
|
+
const resource = makeResource();
|
|
49
|
+
const state: Record<string, unknown> = {};
|
|
50
|
+
|
|
51
|
+
// Seed the collection
|
|
52
|
+
state["openapi:collections:pets"] = [];
|
|
53
|
+
state["openapi:counter:pets"] = 0;
|
|
54
|
+
|
|
55
|
+
const create = createCreateGenerator(resource);
|
|
56
|
+
const read = createReadGenerator(resource);
|
|
57
|
+
const update = createUpdateGenerator(resource);
|
|
58
|
+
const list = createListGenerator(resource);
|
|
59
|
+
const del = createDeleteGenerator(resource);
|
|
60
|
+
|
|
61
|
+
// Create
|
|
62
|
+
const createResult = create(
|
|
63
|
+
makeContext({
|
|
64
|
+
method: "POST",
|
|
65
|
+
path: "/pets",
|
|
66
|
+
body: { name: "Buddy" },
|
|
67
|
+
state,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
expect(createResult).toEqual([201, { name: "Buddy", petId: 1 }]);
|
|
71
|
+
|
|
72
|
+
// Read
|
|
73
|
+
const readResult = read(
|
|
74
|
+
makeContext({
|
|
75
|
+
path: "/pets/1",
|
|
76
|
+
params: { petId: "1" },
|
|
77
|
+
state,
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
expect(readResult).toEqual({ name: "Buddy", petId: 1 });
|
|
81
|
+
|
|
82
|
+
// Update
|
|
83
|
+
const updateResult = update(
|
|
84
|
+
makeContext({
|
|
85
|
+
method: "PUT",
|
|
86
|
+
path: "/pets/1",
|
|
87
|
+
params: { petId: "1" },
|
|
88
|
+
body: { name: "Max" },
|
|
89
|
+
state,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
expect(updateResult).toEqual({ name: "Max", petId: 1 });
|
|
93
|
+
|
|
94
|
+
// List
|
|
95
|
+
const listResult = list(makeContext({ state }));
|
|
96
|
+
expect(listResult).toEqual([{ name: "Max", petId: 1 }]);
|
|
97
|
+
|
|
98
|
+
// Delete
|
|
99
|
+
const deleteResult = del(
|
|
100
|
+
makeContext({
|
|
101
|
+
method: "DELETE",
|
|
102
|
+
path: "/pets/1",
|
|
103
|
+
params: { petId: "1" },
|
|
104
|
+
state,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
expect(deleteResult).toEqual([204, undefined]);
|
|
108
|
+
|
|
109
|
+
// Verify deletion
|
|
110
|
+
const afterDelete = list(makeContext({ state }));
|
|
111
|
+
expect(afterDelete).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("read generator", () => {
|
|
116
|
+
it("returns 404 for missing resources", () => {
|
|
117
|
+
const resource = makeResource();
|
|
118
|
+
const state: Record<string, unknown> = {
|
|
119
|
+
"openapi:collections:pets": [],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const read = createReadGenerator(resource);
|
|
123
|
+
const result = read(
|
|
124
|
+
makeContext({
|
|
125
|
+
path: "/pets/999",
|
|
126
|
+
params: { petId: "999" },
|
|
127
|
+
state,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
expect(result).toEqual([404, { error: "Not found", code: "NOT_FOUND" }]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("update generator", () => {
|
|
135
|
+
it("returns 404 for missing resources", () => {
|
|
136
|
+
const resource = makeResource();
|
|
137
|
+
const state: Record<string, unknown> = {
|
|
138
|
+
"openapi:collections:pets": [],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const update = createUpdateGenerator(resource);
|
|
142
|
+
const result = update(
|
|
143
|
+
makeContext({
|
|
144
|
+
method: "PUT",
|
|
145
|
+
path: "/pets/999",
|
|
146
|
+
params: { petId: "999" },
|
|
147
|
+
body: { name: "Ghost" },
|
|
148
|
+
state,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
expect(result).toEqual([404, { error: "Not found", code: "NOT_FOUND" }]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("delete generator", () => {
|
|
156
|
+
it("returns 404 for missing resources", () => {
|
|
157
|
+
const resource = makeResource();
|
|
158
|
+
const state: Record<string, unknown> = {
|
|
159
|
+
"openapi:collections:pets": [],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const del = createDeleteGenerator(resource);
|
|
163
|
+
const result = del(
|
|
164
|
+
makeContext({
|
|
165
|
+
method: "DELETE",
|
|
166
|
+
path: "/pets/999",
|
|
167
|
+
params: { petId: "999" },
|
|
168
|
+
state,
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
expect(result).toEqual([404, { error: "Not found", code: "NOT_FOUND" }]);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("create generator", () => {
|
|
176
|
+
it("auto-increments IDs", () => {
|
|
177
|
+
const resource = makeResource();
|
|
178
|
+
const state: Record<string, unknown> = {
|
|
179
|
+
"openapi:collections:pets": [],
|
|
180
|
+
"openapi:counter:pets": 0,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const create = createCreateGenerator(resource);
|
|
184
|
+
create(makeContext({ method: "POST", body: { name: "A" }, state }));
|
|
185
|
+
create(makeContext({ method: "POST", body: { name: "B" }, state }));
|
|
186
|
+
|
|
187
|
+
const collection = state["openapi:collections:pets"] as Record<
|
|
188
|
+
string,
|
|
189
|
+
unknown
|
|
190
|
+
>[];
|
|
191
|
+
expect(collection[0].petId).toBe(1);
|
|
192
|
+
expect(collection[1].petId).toBe(2);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("generateSeedItems", () => {
|
|
197
|
+
it("generates items with auto-assigned IDs", () => {
|
|
198
|
+
const schema = {
|
|
199
|
+
type: "object" as const,
|
|
200
|
+
properties: {
|
|
201
|
+
name: { type: "string" as const },
|
|
202
|
+
},
|
|
203
|
+
required: ["name" as const],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const items = generateSeedItems(schema, 3, "petId");
|
|
207
|
+
expect(items).toHaveLength(3);
|
|
208
|
+
for (let i = 0; i < 3; i++) {
|
|
209
|
+
const item = items[i] as Record<string, unknown>;
|
|
210
|
+
expect(item.petId).toBe(i + 1);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|