kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 9a620ba2f31238f03cd28f1da5ef3838d67e4e8a 249 lines 6.6 kB view raw
1import { describe, expect, it } from "vitest"; 2import { 3 dedupeOperationIds, 4 ensureOperationSummaries, 5 markOptionalSchemaFieldsNullable, 6 mergeOpenApiSpecs, 7 normalizeApiServerUrl, 8 normalizeEmptyRequiredArrays, 9 normalizeNullableSchemasForOpenApi30, 10 normalizeOrganizationAuthOperations, 11} from "../../../apps/api/src/utils/openapi-spec"; 12 13describe("openapi spec helpers", () => { 14 it("normalizes API server urls", () => { 15 expect(normalizeApiServerUrl("https://api.kaneo.app")).toBe( 16 "https://api.kaneo.app/api", 17 ); 18 expect(normalizeApiServerUrl("https://api.kaneo.app/api/")).toBe( 19 "https://api.kaneo.app/api", 20 ); 21 }); 22 23 it("normalizes organization auth operations and prunes components", () => { 24 const authSpec = { 25 paths: { 26 "/organization/list-members": { 27 get: { 28 operationId: "oldId", 29 summary: "Old summary", 30 responses: { 31 200: { 32 content: { 33 "application/json": { 34 schema: { $ref: "#/components/schemas/MemberList" }, 35 }, 36 }, 37 }, 38 }, 39 }, 40 }, 41 "/session/get": { 42 get: { 43 operationId: "ignored", 44 }, 45 }, 46 }, 47 security: [{ bearerAuth: [] }], 48 components: { 49 securitySchemes: { 50 bearerAuth: { type: "http", scheme: "bearer" }, 51 }, 52 schemas: { 53 MemberList: { 54 type: "object", 55 properties: { 56 data: { 57 $ref: "#/components/schemas/Member", 58 }, 59 }, 60 }, 61 Member: { 62 type: "object", 63 properties: { 64 id: { type: "string" }, 65 }, 66 }, 67 Ignored: { 68 type: "object", 69 }, 70 }, 71 }, 72 }; 73 74 const normalized = normalizeOrganizationAuthOperations(authSpec); 75 76 expect(Object.keys(normalized.paths as Record<string, unknown>)).toEqual([ 77 "/auth/organization/list-members", 78 ]); 79 80 const operation = ( 81 normalized.paths as Record< 82 string, 83 Record<string, Record<string, unknown>> 84 > 85 )["/auth/organization/list-members"].get; 86 87 expect(operation.operationId).toBe("listOrganizationMembers"); 88 expect(operation.summary).toBe("List Organization Members"); 89 expect(operation.tags).toEqual(["Organization Management"]); 90 91 const schemaNames = Object.keys( 92 ( 93 normalized.components as { 94 schemas?: Record<string, unknown>; 95 } 96 ).schemas || {}, 97 ); 98 expect(schemaNames).toEqual(["MemberList", "Member"]); 99 }); 100 101 it("merges hono and auth specs", () => { 102 const merged = mergeOpenApiSpecs( 103 { 104 openapi: "3.1.0", 105 info: { title: "API" }, 106 paths: { "/tasks": { get: { operationId: "getTasks" } } }, 107 tags: [{ name: "Tasks" }], 108 components: { 109 schemas: { Task: { type: "object" } }, 110 }, 111 }, 112 { 113 paths: { "/auth/session": { get: { operationId: "getSession" } } }, 114 tags: [{ name: "Auth" }], 115 components: { 116 securitySchemes: { bearerAuth: { type: "http" } }, 117 schemas: { Session: { type: "object" } }, 118 }, 119 }, 120 ); 121 122 expect(merged.openapi).toBe("3.1.0"); 123 expect(Object.keys(merged.paths)).toEqual(["/tasks", "/auth/session"]); 124 expect(merged.tags).toEqual([{ name: "Tasks" }, { name: "Auth" }]); 125 expect(merged.components.schemas).toEqual({ 126 Task: { type: "object" }, 127 Session: { type: "object" }, 128 }); 129 expect(merged.components.securitySchemes).toEqual({ 130 bearerAuth: { type: "http" }, 131 }); 132 }); 133 134 it("dedupes operation ids using method and path", () => { 135 const spec = dedupeOperationIds({ 136 paths: { 137 "/tasks": { 138 get: { operationId: "getTask" }, 139 }, 140 "/tasks/{id}": { 141 get: { operationId: "getTask" }, 142 }, 143 }, 144 }); 145 146 expect( 147 (spec.paths as Record<string, Record<string, { operationId: string }>>)[ 148 "/tasks/{id}" 149 ].get.operationId, 150 ).toBe("getTask_get_tasks_id"); 151 }); 152 153 it("normalizes nullable schemas and empty required arrays", () => { 154 const spec = normalizeEmptyRequiredArrays( 155 normalizeNullableSchemasForOpenApi30({ 156 components: { 157 schemas: { 158 Example: { 159 type: ["string", "null"], 160 required: [], 161 }, 162 ExampleAnyOf: { 163 anyOf: [{ type: "null" }, { type: "number", minimum: 1 }], 164 }, 165 }, 166 }, 167 }), 168 ); 169 170 expect( 171 ( 172 spec.components as { 173 schemas: Record<string, Record<string, unknown>>; 174 } 175 ).schemas.Example, 176 ).toEqual({ 177 type: "string", 178 nullable: true, 179 }); 180 181 expect( 182 ( 183 spec.components as { 184 schemas: Record<string, Record<string, unknown>>; 185 } 186 ).schemas.ExampleAnyOf, 187 ).toEqual({ 188 type: "number", 189 minimum: 1, 190 nullable: true, 191 }); 192 }); 193 194 it("marks optional schema fields nullable and fills missing summaries", () => { 195 const spec = ensureOperationSummaries( 196 markOptionalSchemaFieldsNullable({ 197 paths: { 198 "/tasks": { 199 get: { 200 operationId: "listWorkspaceTasks", 201 }, 202 }, 203 }, 204 components: { 205 schemas: { 206 Task: { 207 type: "object", 208 required: ["id"], 209 properties: { 210 id: { type: "string" }, 211 title: { type: "string" }, 212 estimate: { type: "number", nullable: true }, 213 }, 214 }, 215 }, 216 }, 217 }), 218 ); 219 220 expect( 221 ( 222 spec.components as { 223 schemas: Record< 224 string, 225 { properties: Record<string, Record<string, unknown>> } 226 >; 227 } 228 ).schemas.Task.properties.title.nullable, 229 ).toBe(true); 230 expect( 231 ( 232 spec.components as { 233 schemas: Record< 234 string, 235 { properties: Record<string, Record<string, unknown>> } 236 >; 237 } 238 ).schemas.Task.properties.id.nullable, 239 ).toBeUndefined(); 240 expect( 241 ( 242 spec.paths as Record< 243 string, 244 Record<string, { summary?: string; operationId: string }> 245 > 246 )["/tasks"].get.summary, 247 ).toBe("List Workspace Tasks"); 248 }); 249});