UNPKG

@budibase/server

Version:
654 lines (601 loc) • 18.2 kB
import { events } from "@budibase/backend-core" import { BodyType, Datasource, SourceName } from "@budibase/types" import fs from "fs" import path from "path" import TestConfig from "../../../../../tests/utilities/TestConfiguration" import { RestImporter } from "../index" type Assertions = Record< DatasetKey, { name?: string; source?: string; count?: number; endpoints?: number } > const getData = (file: string) => { return fs.readFileSync( path.join(__dirname, `../sources/tests/${file}`), "utf8" ) } // openapi2 (swagger) const oapi2CrudJson = getData("openapi2/data/crud/crud.json") const oapi2CrudYaml = getData("openapi2/data/crud/crud.json") const oapi2PetstoreJson = getData("openapi2/data/petstore/petstore.json") const oapi2PetstoreYaml = getData("openapi2/data/petstore/petstore.json") // openapi3 const oapi3CrudJson = getData("openapi3/data/crud/crud.json") const oapi3CrudYaml = getData("openapi3/data/crud/crud.json") const oapi3PetstoreJson = getData("openapi3/data/petstore/petstore.json") const oapi3PetstoreYaml = getData("openapi3/data/petstore/petstore.json") const oapi3OktaYaml = getData("openapi3/data/okta/okta.yaml") // curl const curl = getData("curl/data/post.txt") const datasets = { // openapi2 (swagger) oapi2CrudJson, oapi2CrudYaml, oapi2PetstoreJson, oapi2PetstoreYaml, // openapi3 oapi3CrudJson, oapi3CrudYaml, oapi3PetstoreJson, oapi3PetstoreYaml, oapi3OktaYaml, // curl curl, } type DatasetKey = keyof typeof datasets describe("Rest Importer", () => { const config = new TestConfig() beforeAll(async () => { await config.init() }) let restImporter: RestImporter const init = async (data: string) => { restImporter = new RestImporter(data) await restImporter.init() } const runTest = async ( test: ( key: DatasetKey, data: string, assertions: Assertions ) => Promise<void>, assertions: Assertions ) => { await config.doInContext(config.devWorkspaceId, async () => { for (let [key, data] of Object.entries(datasets)) { await test(key as DatasetKey, data, assertions) } }) } const testGetInfo = async ( key: DatasetKey, data: string, assertions: Assertions ) => { await init(data) const info = await restImporter.getInfo() expect(info.name).toBe(assertions[key].name) if (assertions[key].endpoints != null) { expect(info.endpoints.length).toBe(assertions[key].endpoints) } else { expect(info.endpoints.length).toBeGreaterThan(0) } } it("gets info", async () => { const assertions: Assertions = { // openapi2 (swagger) oapi2CrudJson: { name: "CRUD", endpoints: 6, }, oapi2CrudYaml: { name: "CRUD", endpoints: 6, }, oapi2PetstoreJson: { name: "Swagger Petstore", endpoints: 20, }, oapi2PetstoreYaml: { name: "Swagger Petstore", endpoints: 20, }, // openapi3 oapi3CrudJson: { name: "CRUD", endpoints: 6, }, oapi3CrudYaml: { name: "CRUD", endpoints: 6, }, oapi3PetstoreJson: { name: "Swagger Petstore - OpenAPI 3.0", endpoints: 19, }, oapi3PetstoreYaml: { name: "Swagger Petstore - OpenAPI 3.0", endpoints: 19, }, oapi3OktaYaml: { name: "Okta Management Sample", endpoints: 2, }, // curl curl: { name: "example.com", endpoints: 1, }, } await runTest(testGetInfo, assertions) }) const testImportQueries = async ( key: DatasetKey, data: string, assertions: Assertions ) => { await init(data) const datasource = await config.createDatasource() const importResult = await config.doInContext(config.devWorkspaceId, () => restImporter.importQueries(datasource._id) ) expect(importResult.errorQueries.length).toBe(0) expect(importResult.queries.length).toBe(assertions[key].count) expect(events.query.imported).toHaveBeenCalledTimes(1) expect(events.query.imported).toHaveBeenCalledWith( datasource, assertions[key].source, assertions[key].count ) jest.clearAllMocks() } it("imports queries", async () => { // simple sanity assertions that the whole dataset // makes it through the importer const assertions: Assertions = { // openapi2 (swagger) oapi2CrudJson: { count: 6, source: "openapi2.0", }, oapi2CrudYaml: { count: 6, source: "openapi2.0", }, oapi2PetstoreJson: { count: 20, source: "openapi2.0", }, oapi2PetstoreYaml: { count: 20, source: "openapi2.0", }, // openapi3 oapi3CrudJson: { count: 6, source: "openapi3.0", }, oapi3CrudYaml: { count: 6, source: "openapi3.0", }, oapi3PetstoreJson: { count: 19, source: "openapi3.0", }, oapi3PetstoreYaml: { count: 19, source: "openapi3.0", }, oapi3OktaYaml: { count: 2, source: "openapi3.0", }, // curl curl: { count: 1, source: "curl", }, } await runTest(testImportQueries, assertions) }) it("imports referenced parameters and request bodies from Okta specs", async () => { await init(oapi3OktaYaml) const datasource = await config.createDatasource() const { queries } = await config.doInContext(config.devWorkspaceId, () => restImporter.importQueries(datasource._id) ) const listQuery = queries.find(query => query.name === "listApps") const createQuery = queries.find(query => query.name === "createApp") expect(listQuery).toBeDefined() expect(createQuery).toBeDefined() const getParameters = listQuery?.parameters || [] expect(getParameters).toEqual( expect.arrayContaining([ expect.objectContaining({ name: "Okta-Version", default: "2025-11-0" }), expect.objectContaining({ name: "limit", default: "200" }), expect.objectContaining({ name: "subdomain", default: "example" }), ]) ) expect(listQuery?.fields.headers?.["Okta-Version"]).toBe("{{Okta-Version}}") expect(listQuery?.fields.queryString).toBe("limit={{limit}}") expect(listQuery?.fields.path).toContain( "https://{{subdomain}}.okta.com/api/v1/apps" ) const createParameters = createQuery?.parameters || [] expect(createParameters).toEqual( expect.arrayContaining([ expect.objectContaining({ name: "Okta-Version", default: "2025-11-0" }), expect.objectContaining({ name: "subdomain", default: "example" }), expect.objectContaining({ name: "label", default: "Example app", }), expect.objectContaining({ name: "settings_oauthClient_client_uri", default: "https://example.okta.com/app", }), expect.objectContaining({ name: "settings_oauthClient_redirect_uris", default: "https://example.okta.com/callback", }), ]) ) expect(createQuery?.fields.bodyType).toBe(BodyType.JSON) expect(createQuery?.fields.path).toContain( "https://{{subdomain}}.okta.com/api/v1/apps" ) expect(createQuery?.fields.requestBody).toContain('"label": "{{ label }}"') expect(createQuery?.fields.requestBody).toContain( '"client_uri": "{{ settings_oauthClient_client_uri }}"' ) expect(createQuery?.fields.requestBody).toContain( "{{ settings_oauthClient_redirect_uris }}" ) expect(createQuery?.restTemplateMetadata?.defaultBindings).toMatchObject({ label: "Example app", settings_oauthClient_client_uri: "https://example.okta.com/app", settings_oauthClient_redirect_uris: "https://example.okta.com/callback", "Okta-Version": "2025-11-0", subdomain: "example", }) jest.clearAllMocks() }) it("imports only the selected endpoint", async () => { const dataset = oapi3CrudJson await init(dataset) const info = await restImporter.getInfo() const endpoint = info.endpoints[0] const datasource = await config.createDatasource() const importResult = await config.doInContext(config.devWorkspaceId, () => restImporter.importQueries(datasource._id, endpoint.id) ) expect(importResult.errorQueries.length).toBe(0) expect(importResult.queries.length).toBe(1) expect(importResult.queries[0].name).toBe(endpoint.name) expect(events.query.imported).toHaveBeenCalledTimes(1) expect(events.query.imported).toHaveBeenCalledWith( datasource, restImporter.source.getImportSource(), 1 ) jest.clearAllMocks() }) it("throws when the selected endpoint is missing", async () => { await init(oapi3CrudJson) const datasource = await config.createDatasource() await expect( config.doInContext(config.devWorkspaceId, () => restImporter.importQueries(datasource._id, "missing::endpoint") ) ).rejects.toThrow("Selected endpoint could not be imported") }) it("filters unsupported options methods", async () => { const spec = JSON.stringify( { swagger: "2.0", info: { title: "Options Filter", }, paths: { "/files": { options: { operationId: "filesOptions", responses: { 200: { description: "OK", }, }, }, get: { operationId: "filesGet", responses: { 200: { description: "OK", }, }, }, }, }, }, null, 2 ) await init(spec) const datasource = await config.createDatasource() const importResult = await config.doInContext(config.devWorkspaceId, () => restImporter.importQueries(datasource._id) ) expect(importResult.errorQueries.length).toBe(0) expect(importResult.queries.length).toBe(1) expect(importResult.queries[0].queryVerb).toBe("read") expect(events.query.imported).toHaveBeenCalledTimes(1) expect(events.query.imported).toHaveBeenCalledWith( datasource, "openapi2.0", 1 ) jest.clearAllMocks() }) it("populates request body for OpenAPI 3 POST endpoints without examples", async () => { const openapi3Doc = { openapi: "3.0.0", info: { title: "Bindings", version: "1.0.0", }, paths: { "/users": { post: { operationId: "createUser", summary: "Creates a user", description: "Creates a user", externalDocs: { url: "https://docs.example.com/users#create", }, parameters: [ { name: "verbose", in: "query", schema: { type: "boolean", default: false, }, }, ], requestBody: { required: true, content: { "application/json": { schema: { type: "object", required: ["name", "email"], properties: { name: { type: "string" }, email: { type: "string" }, age: { type: "integer" }, verified: { type: "boolean" }, }, }, }, }, }, responses: { "200": { description: "successful operation", }, }, }, }, }, } await init(JSON.stringify(openapi3Doc)) const datasource = await config.createDatasource() const { queries: importedQueries } = await config.doInContext( config.devWorkspaceId, () => restImporter.importQueries(datasource._id) ) const createQuery = importedQueries.find( query => query.name === "createUser" ) expect(createQuery).toBeDefined() expect(createQuery?.fields.requestBody).toBeDefined() const expectedBody = `{ "name": "{{ name }}", "email": "{{ email }}", "age": {{ age }}, "verified": {{ verified }} }` expect(createQuery?.fields.requestBody).toEqual(expectedBody) expect(createQuery?.parameters).toEqual([ { name: "verbose", default: "false" }, { name: "name", default: "" }, { name: "email", default: "" }, { name: "age", default: "0" }, { name: "verified", default: "false" }, ]) expect(createQuery?.restTemplateMetadata).toMatchObject({ operationId: "createUser", docsUrl: "https://docs.example.com/users#create", description: "Creates a user", originalPath: "/users", originalRequestBody: { name: "{{ name }}", email: "{{ email }}", age: "{{ age }}", verified: "{{ verified }}", }, defaultBindings: { name: "", email: "", age: "0", verified: "false", verbose: "false", }, }) }) it("populates request body for OpenAPI 2 POST endpoints without examples", async () => { const openapi2Doc = { swagger: "2.0", info: { title: "Bindings", version: "1.0.0", }, paths: { "/users": { post: { operationId: "createUserV2", consumes: ["application/json"], parameters: [ { name: "user", in: "body", required: true, schema: { type: "object", required: ["name", "email"], properties: { name: { type: "string" }, email: { type: "string" }, age: { type: "integer" }, verified: { type: "boolean" }, }, }, }, ], responses: { "200": { description: "successful operation", }, }, }, }, }, } await init(JSON.stringify(openapi2Doc)) const datasource = await config.createDatasource() const { queries: importedQueries } = await config.doInContext( config.devWorkspaceId, () => restImporter.importQueries(datasource._id) ) const createQuery = importedQueries.find( query => query.name === "createUserV2" ) expect(createQuery).toBeDefined() expect(createQuery?.fields.requestBody).toBeDefined() const expectedBody = `{ "name": "{{ name }}", "email": "{{ email }}", "age": {{ age }}, "verified": {{ verified }} }` expect(createQuery?.fields.requestBody).toEqual(expectedBody) expect(createQuery?.parameters).toEqual([ { name: "name", default: "" }, { name: "email", default: "" }, { name: "age", default: "0" }, { name: "verified", default: "false" }, ]) }) it("returns OpenAPI 3 server variable defaults before importing queries", async () => { const openapiWithServerVariables = { openapi: "3.0.0", info: { title: "Server Variables", version: "1.0.0", }, servers: [ { url: "https://{companyDomain}.example.com", variables: { companyDomain: { default: "acme", description: "Company domain", }, }, }, ], paths: { "/users": { get: { operationId: "getUsers", responses: { "200": { description: "successful operation", }, }, }, }, }, } await init(JSON.stringify(openapiWithServerVariables)) const staticVariables = restImporter.getStaticServerVariables() expect(staticVariables).toEqual({ companyDomain: "acme" }) }) const openapiWithHeaderSecurity = { openapi: "3.0.0", info: { title: "Header Security", version: "1.0.0", }, components: { securitySchemes: { ApiKeyAuth: { type: "apiKey", in: "header", name: "X-Apikey", }, }, }, security: [{ ApiKeyAuth: [] }], paths: { "/files": { get: { operationId: "listFiles", parameters: [ { name: "x-apikey", in: "header", schema: { type: "string", }, }, ], responses: { "200": { description: "OK", }, }, }, }, }, } it("adds datasource header defaults from OpenAPI security schemes", async () => { await init(JSON.stringify(openapiWithHeaderSecurity)) const datasource: Datasource = { type: "datasource", source: SourceName.REST, config: {}, } restImporter.prepareDatasourceConfig(datasource) expect(datasource.config?.defaultHeaders).toEqual({ "X-Apikey": "" }) }) it("does not duplicate security headers in generated queries", async () => { await init(JSON.stringify(openapiWithHeaderSecurity)) const datasource = await config.createDatasource() const importResult = await config.doInContext(config.devWorkspaceId, () => restImporter.importQueries(datasource._id) ) expect(importResult.queries.length).toBe(1) const [query] = importResult.queries expect(query.parameters?.some(param => param.name === "x-apikey")).toBe( false ) expect(query.fields.headers?.["X-Apikey"]).toBeUndefined() }) it("exposes security headers via importer info", async () => { await init(JSON.stringify(openapiWithHeaderSecurity)) const info = await restImporter.getInfo() expect(info.securityHeaders).toEqual(["X-Apikey"]) }) })