microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
packages/openapi3/test/openapi-output.test.ts
242lines · modecode
| 1 | import { resolvePath } from "@cadl-lang/compiler"; |
| 2 | import { expectDiagnosticEmpty } from "@cadl-lang/compiler/testing"; |
| 3 | import { deepStrictEqual, ok, strictEqual } from "assert"; |
| 4 | import { OpenAPI3EmitterOptions } from "../src/lib.js"; |
| 5 | import { OpenAPI3Document } from "../src/types.js"; |
| 6 | import { createOpenAPITestRunner, oapiForModel, openApiFor } from "./test-host.js"; |
| 7 | |
| 8 | describe("openapi3: types included", () => { |
| 9 | async function openapiWithOptions( |
| 10 | code: string, |
| 11 | options: OpenAPI3EmitterOptions |
| 12 | ): Promise<OpenAPI3Document> { |
| 13 | const runner = await createOpenAPITestRunner(); |
| 14 | |
| 15 | const outPath = resolvePath("/openapi.json"); |
| 16 | |
| 17 | const diagnostics = await runner.diagnose(code, { |
| 18 | noEmit: false, |
| 19 | emit: ["@cadl-lang/openapi3"], |
| 20 | options: { "@cadl-lang/openapi3": { ...options, "output-file": outPath } }, |
| 21 | }); |
| 22 | |
| 23 | expectDiagnosticEmpty(diagnostics.filter((x) => x.code !== "@cadl-lang/rest/no-routes")); |
| 24 | |
| 25 | const content = runner.fs.get(outPath)!; |
| 26 | return JSON.parse(content); |
| 27 | } |
| 28 | |
| 29 | it("emit unreferenced types by default", async () => { |
| 30 | const output = await openapiWithOptions( |
| 31 | ` |
| 32 | model NotReferenced {name: string} |
| 33 | model Referenced {name: string} |
| 34 | |
| 35 | op test(): Referenced; |
| 36 | `, |
| 37 | {} |
| 38 | ); |
| 39 | deepStrictEqual(Object.keys(output.components!.schemas!), ["NotReferenced", "Referenced"]); |
| 40 | }); |
| 41 | |
| 42 | it("emit only referenced types when using omit-unreachable-types", async () => { |
| 43 | const output = await openapiWithOptions( |
| 44 | ` |
| 45 | model NotReferenced {name: string} |
| 46 | model Referenced {name: string} |
| 47 | |
| 48 | op test(): Referenced; |
| 49 | `, |
| 50 | { |
| 51 | "omit-unreachable-types": true, |
| 52 | } |
| 53 | ); |
| 54 | deepStrictEqual(Object.keys(output.components!.schemas!), ["Referenced"]); |
| 55 | }); |
| 56 | }); |
| 57 | |
| 58 | describe("openapi3: literals", () => { |
| 59 | const cases = [ |
| 60 | ["1", { type: "number", enum: [1] }], |
| 61 | ['"hello"', { type: "string", enum: ["hello"] }], |
| 62 | ["false", { type: "boolean", enum: [false] }], |
| 63 | ["true", { type: "boolean", enum: [true] }], |
| 64 | ]; |
| 65 | |
| 66 | for (const test of cases) { |
| 67 | it("knows schema for " + test[0], async () => { |
| 68 | const res = await oapiForModel( |
| 69 | "Pet", |
| 70 | ` |
| 71 | model Pet { name: ${test[0]} }; |
| 72 | ` |
| 73 | ); |
| 74 | |
| 75 | const schema = res.schemas.Pet.properties.name; |
| 76 | deepStrictEqual(schema, test[1]); |
| 77 | }); |
| 78 | } |
| 79 | }); |
| 80 | |
| 81 | describe("openapi3: operations", () => { |
| 82 | it("define operations with param with defaults", async () => { |
| 83 | const res = await openApiFor( |
| 84 | ` |
| 85 | @route("/") |
| 86 | @get() |
| 87 | op read(@query queryWithDefault?: string = "defaultValue"): string; |
| 88 | ` |
| 89 | ); |
| 90 | |
| 91 | deepStrictEqual(res.paths["/"].get.parameters[0].schema.default, "defaultValue"); |
| 92 | }); |
| 93 | |
| 94 | it("define operations with param with decorators", async () => { |
| 95 | const res = await openApiFor( |
| 96 | ` |
| 97 | @get |
| 98 | @route("/thing/{name}") |
| 99 | op getThing( |
| 100 | @pattern("^[a-zA-Z0-9-]{3,24}$") |
| 101 | @path name: string, |
| 102 | |
| 103 | @minValue(1) |
| 104 | @maxValue(10) |
| 105 | @query count: int32 |
| 106 | ): string; |
| 107 | ` |
| 108 | ); |
| 109 | |
| 110 | const getThing = res.paths["/thing/{name}"].get; |
| 111 | ok(getThing); |
| 112 | ok(getThing.parameters[0].schema); |
| 113 | ok(getThing.parameters[0].schema.pattern); |
| 114 | strictEqual(getThing.parameters[0].schema.pattern, "^[a-zA-Z0-9-]{3,24}$"); |
| 115 | |
| 116 | ok(getThing.parameters[1].schema); |
| 117 | ok(getThing.parameters[1].schema.minimum); |
| 118 | ok(getThing.parameters[1].schema.maximum); |
| 119 | strictEqual(getThing.parameters[1].schema.minimum, 1); |
| 120 | strictEqual(getThing.parameters[1].schema.maximum, 10); |
| 121 | }); |
| 122 | |
| 123 | it("deprecate operations with @deprecated", async () => { |
| 124 | const res = await openApiFor( |
| 125 | ` |
| 126 | @deprecated("use something else") |
| 127 | op read(@query query: string): string; |
| 128 | ` |
| 129 | ); |
| 130 | |
| 131 | strictEqual(res.paths["/"].get.deprecated, true); |
| 132 | }); |
| 133 | }); |
| 134 | |
| 135 | describe("openapi3: request", () => { |
| 136 | describe("binary request", () => { |
| 137 | it("bytes request should default to application/json byte", async () => { |
| 138 | const res = await openApiFor(` |
| 139 | @post op read(@body body: bytes): {}; |
| 140 | `); |
| 141 | |
| 142 | const requestBody = res.paths["/"].post.requestBody; |
| 143 | ok(requestBody); |
| 144 | strictEqual(requestBody.content["application/json"].schema.type, "string"); |
| 145 | strictEqual(requestBody.content["application/json"].schema.format, "byte"); |
| 146 | }); |
| 147 | |
| 148 | it("bytes request should respect @header contentType and use binary format when not json or text", async () => { |
| 149 | const res = await openApiFor(` |
| 150 | @post op read(@header contentType: "image/png", @body body: bytes): {}; |
| 151 | `); |
| 152 | |
| 153 | const requestBody = res.paths["/"].post.requestBody; |
| 154 | ok(requestBody); |
| 155 | strictEqual(requestBody.content["image/png"].schema.type, "string"); |
| 156 | strictEqual(requestBody.content["image/png"].schema.format, "binary"); |
| 157 | }); |
| 158 | }); |
| 159 | }); |
| 160 | |
| 161 | describe("openapi3: extension decorator", () => { |
| 162 | it("adds an arbitrary extension to a model", async () => { |
| 163 | const oapi = await openApiFor(` |
| 164 | @extension("x-model-extension", "foobar") |
| 165 | model Pet { |
| 166 | name: string; |
| 167 | } |
| 168 | @get() op read(): Pet; |
| 169 | `); |
| 170 | ok(oapi.components.schemas.Pet); |
| 171 | strictEqual(oapi.components.schemas.Pet["x-model-extension"], "foobar"); |
| 172 | }); |
| 173 | |
| 174 | it("adds an arbitrary extension to an operation", async () => { |
| 175 | const oapi = await openApiFor( |
| 176 | ` |
| 177 | model Pet { |
| 178 | name: string; |
| 179 | } |
| 180 | @get() |
| 181 | @extension("x-operation-extension", "barbaz") |
| 182 | op list(): Pet[]; |
| 183 | ` |
| 184 | ); |
| 185 | ok(oapi.paths["/"].get); |
| 186 | strictEqual(oapi.paths["/"].get["x-operation-extension"], "barbaz"); |
| 187 | }); |
| 188 | |
| 189 | it("adds an arbitrary extension to a parameter", async () => { |
| 190 | const oapi = await openApiFor( |
| 191 | ` |
| 192 | model Pet { |
| 193 | name: string; |
| 194 | } |
| 195 | model PetId { |
| 196 | @path |
| 197 | @extension("x-parameter-extension", "foobaz") |
| 198 | petId: string; |
| 199 | } |
| 200 | @route("/Pets") |
| 201 | @get() |
| 202 | op get(... PetId): Pet; |
| 203 | ` |
| 204 | ); |
| 205 | ok(oapi.paths["/Pets/{petId}"].get); |
| 206 | strictEqual( |
| 207 | oapi.paths["/Pets/{petId}"].get.parameters[0]["$ref"], |
| 208 | "#/components/parameters/PetId" |
| 209 | ); |
| 210 | strictEqual(oapi.components.parameters.PetId.name, "petId"); |
| 211 | strictEqual(oapi.components.parameters.PetId["x-parameter-extension"], "foobaz"); |
| 212 | }); |
| 213 | |
| 214 | it("check format and pattern decorator on model", async () => { |
| 215 | const oapi = await openApiFor( |
| 216 | ` |
| 217 | model Pet extends PetId { |
| 218 | @pattern("^[a-zA-Z0-9-]{3,24}$") |
| 219 | name: string; |
| 220 | } |
| 221 | model PetId { |
| 222 | @path |
| 223 | @pattern("^[a-zA-Z0-9-]{3,24}$") |
| 224 | @format("UUID") |
| 225 | petId: string; |
| 226 | } |
| 227 | @route("/Pets") |
| 228 | @get() |
| 229 | op get(... PetId): Pet; |
| 230 | ` |
| 231 | ); |
| 232 | ok(oapi.paths["/Pets/{petId}"].get); |
| 233 | strictEqual( |
| 234 | oapi.paths["/Pets/{petId}"].get.parameters[0]["$ref"], |
| 235 | "#/components/parameters/PetId" |
| 236 | ); |
| 237 | strictEqual(oapi.components.parameters.PetId.name, "petId"); |
| 238 | strictEqual(oapi.components.schemas.Pet.properties.name.pattern, "^[a-zA-Z0-9-]{3,24}$"); |
| 239 | strictEqual(oapi.components.parameters.PetId.schema.format, "UUID"); |
| 240 | strictEqual(oapi.components.parameters.PetId.schema.pattern, "^[a-zA-Z0-9-]{3,24}$"); |
| 241 | }); |
| 242 | }); |
| 243 | |