Adding a testing system which generates the schema and compares against responses.

This commit is contained in:
Michael Drury 2022-02-24 23:21:10 +00:00
parent f2c2c903e5
commit 46d23cfb25
17 changed files with 2698 additions and 2630 deletions

View File

@ -53,10 +53,10 @@
to-gfm-code-block "^0.1.1"
year "^0.2.1"
"@budibase/string-templates@^1.0.66-alpha.0":
version "1.0.72"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.72.tgz#acc154e402cce98ea30eedde9c6124183ee9b37c"
integrity sha512-w715TjgO6NUHkZNqoOEo8lAKJ/PQ4b00ATWSX5VB523SAu7y/uOiqKqV1E3fgwxq1o8L+Ff7rn9FTkiYtjkV/g==
"@budibase/string-templates@^1.0.72-alpha.0":
version "1.0.75"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.75.tgz#5b4061f1a626160ec092f32f036541376298100c"
integrity sha512-hPgr6n5cpSCGFEha5DS/P+rtRXOLc72M6y4J/scl59JvUi/ZUJkjRgJdpQPdBLu04CNKp89V59+rAqAuDjOC0g==
dependencies:
"@budibase/handlebars-helpers" "^0.11.7"
dayjs "^1.10.4"

File diff suppressed because it is too large Load Diff

View File

@ -53,6 +53,7 @@ module FetchMock {
{
doc: {
_id: "test",
tableId: opts.body.split("tableId:")[1].split('"')[0],
},
},
],

View File

@ -158,6 +158,7 @@
"docker-compose": "^0.23.6",
"eslint": "^6.8.0",
"jest": "^27.0.5",
"jest-openapi": "^0.14.2",
"nodemon": "^2.0.4",
"openapi-types": "^9.3.1",
"path-to-regexp": "^6.2.0",

View File

@ -49,12 +49,11 @@ const options = {
apis: [join(__dirname, "..", "src", "api", "routes", "public", "*.ts")],
}
function writeFile(output, { isJson } = {}) {
function writeFile(output, filename) {
try {
const filename = isJson ? "openapi.json" : "openapi.yaml"
const path = join(__dirname, filename)
let spec = output
if (isJson) {
if (filename.endsWith("json")) {
spec = JSON.stringify(output, null, 2)
}
// input the static variables
@ -63,13 +62,22 @@ function writeFile(output, { isJson } = {}) {
}
writeFileSync(path, spec)
console.log(`Wrote spec to ${path}`)
return path
} catch (err) {
console.error(err)
}
}
const outputJSON = swaggerJsdoc(options)
options.format = ".yaml"
const outputYAML = swaggerJsdoc(options)
writeFile(outputJSON, { isJson: true })
writeFile(outputYAML)
function run() {
const outputJSON = swaggerJsdoc(options)
options.format = ".yaml"
const outputYAML = swaggerJsdoc(options)
writeFile(outputJSON, "openapi.json")
return writeFile(outputYAML, "openapi.yaml")
}
if (require.main === module) {
run()
}
module.exports = run

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -63,6 +63,7 @@ const baseColumnDef = {
const tableSchema = {
description: "The table to be created/updated.",
type: "object",
required: ["name", "schema"],
properties: {
name: {
description: "The name of the table",
@ -74,82 +75,85 @@ const tableSchema = {
"The name of the column which should be used in relationship tags when relating to this table.",
},
schema: {
oneOf: [
// relationship
{
type: "object",
properties: {
...baseColumnDef,
type: {
type: "string",
enum: [FieldTypes.LINK],
description: "A relationship column.",
},
fieldName: {
type: "string",
description:
"The name of the column which a relationship column is related to in another table.",
},
tableId: {
type: "string",
description:
"The ID of the table which a relationship column is related to.",
},
relationshipType: {
type: "string",
enum: Object.values(RelationshipTypes),
description:
"Defines the type of relationship that this column will be used for.",
},
through: {
type: "string",
description:
"When using a SQL table that contains many to many relationships this defines the table the relationships are linked through.",
},
foreignKey: {
type: "string",
description:
"When using a SQL table that contains a one to many relationship this defines the foreign key.",
},
throughFrom: {
type: "string",
description:
"When using a SQL table that utilises a through table, this defines the primary key in the through table for this table.",
},
throughTo: {
type: "string",
description:
"When using a SQL table that utilises a through table, this defines the primary key in the through table for the related table.",
type: "object",
additionalProperties: {
oneOf: [
// relationship
{
type: "object",
properties: {
...baseColumnDef,
type: {
type: "string",
enum: [FieldTypes.LINK],
description: "A relationship column.",
},
fieldName: {
type: "string",
description:
"The name of the column which a relationship column is related to in another table.",
},
tableId: {
type: "string",
description:
"The ID of the table which a relationship column is related to.",
},
relationshipType: {
type: "string",
enum: Object.values(RelationshipTypes),
description:
"Defines the type of relationship that this column will be used for.",
},
through: {
type: "string",
description:
"When using a SQL table that contains many to many relationships this defines the table the relationships are linked through.",
},
foreignKey: {
type: "string",
description:
"When using a SQL table that contains a one to many relationship this defines the foreign key.",
},
throughFrom: {
type: "string",
description:
"When using a SQL table that utilises a through table, this defines the primary key in the through table for this table.",
},
throughTo: {
type: "string",
description:
"When using a SQL table that utilises a through table, this defines the primary key in the through table for the related table.",
},
},
},
},
{
type: "object",
properties: {
...baseColumnDef,
type: {
type: "string",
enum: [FieldTypes.FORMULA],
description: "A formula column.",
},
formula: {
type: "string",
description:
"Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format.",
},
formulaType: {
type: "string",
enum: Object.values(FormulaTypes),
description:
"Defines whether this is a static or dynamic formula.",
{
type: "object",
properties: {
...baseColumnDef,
type: {
type: "string",
enum: [FieldTypes.FORMULA],
description: "A formula column.",
},
formula: {
type: "string",
description:
"Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format.",
},
formulaType: {
type: "string",
enum: Object.values(FormulaTypes),
description:
"Defines whether this is a static or dynamic formula.",
},
},
},
},
{
type: "object",
properties: baseColumnDef,
},
],
{
type: "object",
properties: baseColumnDef,
},
],
},
},
},
}

View File

@ -2,6 +2,7 @@ exports.object = (props, opts) => {
return {
type: "object",
properties: props,
required: Object.keys(props),
...opts,
}
}

View File

@ -5,43 +5,6 @@ const { nameValidator, applicationValidator } = require("../utils/validators")
const read = [],
write = []
/**
* @openapi
* /applications/search:
* post:
* summary: Search for an application based on its app name.
* tags:
* - applications
* parameters:
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/nameSearch'
* responses:
* 200:
* description: Returns the applications that were found based on the search parameters.
* content:
* application/json:
* schema:
* type: object
* properties:
* applications:
* type: array
* items:
* $ref: '#/components/schemas/application'
* examples:
* applications:
* $ref: '#/components/examples/applications'
*/
read.push(
new Endpoint("post", "/applications/search", controller.search).addMiddleware(
nameValidator()
)
)
/**
* @openapi
* /applications:
@ -68,7 +31,11 @@ read.push(
* application:
* $ref: '#/components/examples/application'
*/
write.push(new Endpoint("post", "/applications", controller.create))
write.push(
new Endpoint("post", "/applications", controller.create).addMiddleware(
applicationValidator
)
)
/**
* @openapi
@ -96,7 +63,11 @@ write.push(new Endpoint("post", "/applications", controller.create))
* application:
* $ref: '#/components/examples/application'
*/
write.push(new Endpoint("put", "/applications/:appId", controller.update))
write.push(
new Endpoint("put", "/applications/:appId", controller.update).addMiddleware(
applicationValidator
)
)
/**
* @openapi
@ -142,4 +113,43 @@ write.push(new Endpoint("delete", "/applications/:appId", controller.destroy))
*/
read.push(new Endpoint("get", "/applications/:appId", controller.read))
/**
* @openapi
* /applications/search:
* post:
* summary: Search for an application based on its app name.
* tags:
* - applications
* parameters:
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/nameSearch'
* responses:
* 200:
* description: Returns the applications that were found based on the search parameters.
* content:
* application/json:
* schema:
* type: object
* required:
* - applications
* properties:
* applications:
* type: array
* items:
* $ref: '#/components/schemas/application'
* examples:
* applications:
* $ref: '#/components/examples/applications'
*/
read.push(
new Endpoint("post", "/applications/search", controller.search).addMiddleware(
nameValidator()
)
)
export default { read, write }

View File

@ -5,43 +5,6 @@ import { nameValidator } from "../utils/validators"
const read = [],
write = []
/**
* @openapi
* /queries/search:
* post:
* summary: Search for a query based on its name.
* tags:
* - queries
* parameters:
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/nameSearch'
* responses:
* 200:
* description: Returns the queries found based on the search parameters.
* content:
* application/json:
* schema:
* type: object
* properties:
* queries:
* type: array
* items:
* $ref: '#/components/schemas/query'
* examples:
* queries:
* $ref: '#/components/examples/queries'
*/
read.push(
new Endpoint("post", "/queries/search", controller.search).addMiddleware(
nameValidator()
)
)
/**
* @openapi
* /queries/{queryId}:
@ -89,4 +52,43 @@ read.push(
*/
write.push(new Endpoint("post", "/queries/:queryId", controller.execute))
/**
* @openapi
* /queries/search:
* post:
* summary: Search for a query based on its name.
* tags:
* - queries
* parameters:
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/nameSearch'
* responses:
* 200:
* description: Returns the queries found based on the search parameters.
* content:
* application/json:
* schema:
* type: object
* required:
* - queries
* properties:
* queries:
* type: array
* items:
* $ref: '#/components/schemas/query'
* examples:
* queries:
* $ref: '#/components/examples/queries'
*/
read.push(
new Endpoint("post", "/queries/search", controller.search).addMiddleware(
nameValidator()
)
)
export default { read, write }

View File

@ -5,130 +5,6 @@ import { externalSearchValidator } from "../utils/validators"
const read = [],
write = []
/**
* @openapi
* /tables/{tableId}/rows/search:
* post:
* summary: Used to search for rows within a table.
* tags:
* - rows
* parameters:
* - $ref: '#/components/parameters/tableId'
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* query:
* type: object
* properties:
* string:
* type: object
* example:
* columnName1: value
* columnName2: value
* description: A map of field name to the string to search for,
* this will look for rows that have a value starting with the
* string value.
* additionalProperties:
* type: string
* description: The value to search for in the column.
* fuzzy:
* type: object
* description: A fuzzy search, only supported by internal tables.
* range:
* type: object
* description: Searches within a range, the format of this must be
* columnName -> [low, high].
* example:
* columnName1: [10, 20]
* equal:
* type: object
* description: Searches for rows that have a column value that is
* exactly the value set.
* notEqual:
* type: object
* description: Searches for any row which does not contain the specified
* column value.
* empty:
* type: object
* description: Searches for rows which do not contain the specified column.
* The object should simply contain keys of the column names, these
* can map to any value.
* example:
* columnName1: ""
* notEmpty:
* type: object
* description: Searches for rows which have the specified column.
* oneOf:
* type: object
* description: Searches for rows which have a column value that is any
* of the specified values. The format of this must be columnName -> [value1, value2].
* paginate:
* type: boolean
* description: Enables pagination, by default this is disabled.
* bookmark:
* oneOf:
* - type: string
* - type: integer
* description: If retrieving another page, the bookmark from the previous request must be supplied.
* limit:
* type: integer
* description: The maximum number of rows to return, useful when paginating, for internal tables this
* will be limited to 1000, for SQL tables it will be 5000.
* sort:
* type: object
* description: A set of parameters describing the sort behaviour of the search.
* properties:
* order:
* type: string
* enum: [ascending, descending]
* description: The order of the sort, by default this is ascending.
* column:
* type: string
* description: The name of the column by which the rows will be sorted.
* type:
* type: string
* enum: [string, number]
* description: Defines whether the column should be treated as a string
* or as numbers when sorting.
* responses:
* 200:
* description: The response will contain an array of rows that match the search parameters.
* content:
* application/json:
* schema:
* type: object
* properties:
* rows:
* description: An array of rows, these will each contain an _id field which can be used
* to update or delete them.
* type: array
* items:
* type: object
* bookmark:
* oneOf:
* - type: string
* - type: integer
* description: If pagination in use, this should be provided.
* hasNextPage:
* description: If pagination in use, this will determine if there is another page to fetch.
* type: boolean
* examples:
* search:
* $ref: '#/components/examples/rows'
*/
read.push(
new Endpoint(
"post",
"/tables/:tableId/rows/search",
controller.search
).addMiddleware(externalSearchValidator())
)
/**
* @openapi
* /tables/{tableId}/rows:
@ -247,4 +123,132 @@ write.push(
*/
read.push(new Endpoint("get", "/tables/:tableId/rows/:rowId", controller.read))
/**
* @openapi
* /tables/{tableId}/rows/search:
* post:
* summary: Used to search for rows within a table.
* tags:
* - rows
* parameters:
* - $ref: '#/components/parameters/tableId'
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - query
* properties:
* query:
* type: object
* properties:
* string:
* type: object
* example:
* columnName1: value
* columnName2: value
* description: A map of field name to the string to search for,
* this will look for rows that have a value starting with the
* string value.
* additionalProperties:
* type: string
* description: The value to search for in the column.
* fuzzy:
* type: object
* description: A fuzzy search, only supported by internal tables.
* range:
* type: object
* description: Searches within a range, the format of this must be
* columnName -> [low, high].
* example:
* columnName1: [10, 20]
* equal:
* type: object
* description: Searches for rows that have a column value that is
* exactly the value set.
* notEqual:
* type: object
* description: Searches for any row which does not contain the specified
* column value.
* empty:
* type: object
* description: Searches for rows which do not contain the specified column.
* The object should simply contain keys of the column names, these
* can map to any value.
* example:
* columnName1: ""
* notEmpty:
* type: object
* description: Searches for rows which have the specified column.
* oneOf:
* type: object
* description: Searches for rows which have a column value that is any
* of the specified values. The format of this must be columnName -> [value1, value2].
* paginate:
* type: boolean
* description: Enables pagination, by default this is disabled.
* bookmark:
* oneOf:
* - type: string
* - type: integer
* description: If retrieving another page, the bookmark from the previous request must be supplied.
* limit:
* type: integer
* description: The maximum number of rows to return, useful when paginating, for internal tables this
* will be limited to 1000, for SQL tables it will be 5000.
* sort:
* type: object
* description: A set of parameters describing the sort behaviour of the search.
* properties:
* order:
* type: string
* enum: [ascending, descending]
* description: The order of the sort, by default this is ascending.
* column:
* type: string
* description: The name of the column by which the rows will be sorted.
* type:
* type: string
* enum: [string, number]
* description: Defines whether the column should be treated as a string
* or as numbers when sorting.
* responses:
* 200:
* description: The response will contain an array of rows that match the search parameters.
* content:
* application/json:
* schema:
* type: object
* required:
* - rows
* properties:
* rows:
* description: An array of rows, these will each contain an _id field which can be used
* to update or delete them.
* type: array
* items:
* type: object
* bookmark:
* oneOf:
* - type: string
* - type: integer
* description: If pagination in use, this should be provided.
* hasNextPage:
* description: If pagination in use, this will determine if there is another page to fetch.
* type: boolean
* examples:
* search:
* $ref: '#/components/examples/rows'
*/
read.push(
new Endpoint(
"post",
"/tables/:tableId/rows/search",
controller.search
).addMiddleware(externalSearchValidator())
)
export default { read, write }

View File

@ -5,43 +5,6 @@ import { tableValidator, nameValidator } from "../utils/validators"
const read = [],
write = []
/**
* @openapi
* /tables/search:
* post:
* summary: Search internal and external tables based on their name.
* tags:
* - tables
* parameters:
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/nameSearch'
* responses:
* 200:
* description: Returns the found tables, based on the search parameters.
* content:
* application/json:
* schema:
* type: object
* properties:
* applications:
* type: array
* items:
* $ref: '#/components/schemas/table'
* examples:
* tables:
* $ref: '#/components/examples/tables'
*/
read.push(
new Endpoint("post", "/tables/search", controller.search).addMiddleware(
nameValidator()
)
)
/**
* @openapi
* /tables:
@ -158,4 +121,43 @@ write.push(new Endpoint("delete", "/tables/:tableId", controller.destroy))
*/
read.push(new Endpoint("get", "/tables/:tableId", controller.read))
/**
* @openapi
* /tables/search:
* post:
* summary: Search internal and external tables based on their name.
* tags:
* - tables
* parameters:
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/nameSearch'
* responses:
* 200:
* description: Returns the found tables, based on the search parameters.
* content:
* application/json:
* schema:
* type: object
* required:
* - tables
* properties:
* tables:
* type: array
* items:
* $ref: '#/components/schemas/table'
* examples:
* tables:
* $ref: '#/components/examples/tables'
*/
read.push(
new Endpoint("post", "/tables/search", controller.search).addMiddleware(
nameValidator()
)
)
export default { read, write }

View File

@ -0,0 +1,74 @@
const jestOpenAPI = require("jest-openapi").default
const generateSchema = require("../../../../../specs/generate")
const setup = require("../../tests/utilities")
const { checkSlashesInUrl } = require("../../../../utilities")
const yamlPath = generateSchema()
jestOpenAPI(yamlPath)
let request = setup.getRequest()
let config = setup.getConfig()
let apiKey, table
beforeAll(async () => {
await config.init()
table = await config.updateTable()
apiKey = await config.generateApiKey()
})
afterAll(setup.afterAll)
async function makeRequest(method, endpoint, body, appId) {
const extraHeaders = {
"x-budibase-api-key": apiKey,
"x-budibase-app-id": appId ? appId : config.getAppId(),
}
const req = request
[method](checkSlashesInUrl(`/api/public/v1/${endpoint}`))
.set(config.defaultHeaders(extraHeaders))
if (body) {
req.send(body)
}
const res = await req.expect("Content-Type", /json/).expect(200)
expect(res.body).toBeDefined()
return res
}
describe("check the applications endpoints", () => {
it("should allow retrieving applications through search", async () => {
const res = await makeRequest("post", "/applications/search")
expect(res).toSatisfyApiSpec()
})
})
describe("check the tables endpoints", () => {
it("should allow retrieving applications through search", async () => {
const res = await makeRequest("post", "/tables/search")
expect(res).toSatisfyApiSpec()
})
})
describe("check the rows endpoints", () => {
it("should allow retrieving applications through search", async () => {
const res = await makeRequest("post", `/tables/${table._id}/rows/search`, {
query: {
},
})
expect(res).toSatisfyApiSpec()
})
})
describe("check the users endpoints", () => {
it("should allow retrieving applications through search", async () => {
const res = await makeRequest("post", "/users/search")
expect(res).toSatisfyApiSpec()
})
})
describe("check the queries endpoints", () => {
it("should allow retrieving applications through search", async () => {
const res = await makeRequest("post", "/queries/search")
expect(res).toSatisfyApiSpec()
})
})

View File

@ -5,40 +5,6 @@ import { nameValidator } from "../utils/validators"
const read = [],
write = []
/**
* @openapi
* /users/search:
* post:
* summary: Search for a user based on their email/username.
* tags:
* - users
* parameters:
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/nameSearch'
* responses:
* 200:
* description: Returns the found users based on search parameters.
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/user'
* examples:
* users:
* $ref: '#/components/examples/users'
*/
read.push(
new Endpoint("post", "/users/search", controller.search).addMiddleware(
nameValidator()
)
)
/**
* @openapi
* /users:
@ -142,4 +108,43 @@ write.push(new Endpoint("delete", "/users/:userId", controller.destroy))
*/
read.push(new Endpoint("get", "/users/:userId", controller.read))
/**
* @openapi
* /users/search:
* post:
* summary: Search for a user based on their email/username.
* tags:
* - users
* parameters:
* - $ref: '#/components/parameters/appId'
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/nameSearch'
* responses:
* 200:
* description: Returns the found users based on search parameters.
* content:
* application/json:
* schema:
* type: object
* required:
* - users
* properties:
* users:
* type: array
* items:
* $ref: '#/components/schemas/user'
* examples:
* users:
* $ref: '#/components/examples/users'
*/
read.push(
new Endpoint("post", "/users/search", controller.search).addMiddleware(
nameValidator()
)
)
export default { read, write }

View File

@ -25,6 +25,8 @@ const { createASession } = require("@budibase/backend-core/sessions")
const { user: userCache } = require("@budibase/backend-core/cache")
const newid = require("../../db/newid")
const context = require("@budibase/backend-core/context")
const { generateDevInfoID, SEPARATOR } = require("@budibase/backend-core/db")
const { encrypt } = require("@budibase/backend-core/encryption")
const GLOBAL_USER_ID = "us_uuid1"
const EMAIL = "babs@babs.com"
@ -83,6 +85,20 @@ class TestConfiguration {
}
}
async generateApiKey(userId = GLOBAL_USER_ID) {
const db = getGlobalDB(TENANT_ID)
const id = generateDevInfoID(userId)
let devInfo
try {
devInfo = await db.get(id)
} catch (err) {
devInfo = { _id: id, userId }
}
devInfo.apiKey = encrypt(`${TENANT_ID}${SEPARATOR}${newid()}`)
await db.put(devInfo)
return devInfo.apiKey
}
async globalUser({
id = GLOBAL_USER_ID,
builder = true,
@ -135,7 +151,7 @@ class TestConfiguration {
cleanup(this.allApps.map(app => app.appId))
}
defaultHeaders() {
defaultHeaders(extras = {}) {
const auth = {
userId: GLOBAL_USER_ID,
sessionId: "sessionid",
@ -154,6 +170,7 @@ class TestConfiguration {
`${Cookies.CurrentApp}=${appToken}`,
],
[Headers.CSRF_TOKEN]: CSRF_TOKEN,
...extras,
}
if (this.appId) {
headers[Headers.APP_ID] = this.appId

File diff suppressed because it is too large Load Diff