Merge pull request #15643 from Budibase/remove-worker-user-mock

Remove mocks from public variant of `users.spec.ts`
This commit is contained in:
Sam Rose 2025-03-03 16:09:13 +00:00 committed by GitHub
commit e93d9889ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 528 additions and 379 deletions

View File

@ -156,6 +156,7 @@
"@types/pouchdb": "6.4.2", "@types/pouchdb": "6.4.2",
"@types/server-destroy": "1.0.1", "@types/server-destroy": "1.0.1",
"@types/supertest": "2.0.14", "@types/supertest": "2.0.14",
"@types/swagger-jsdoc": "^6.0.4",
"@types/tar": "6.1.5", "@types/tar": "6.1.5",
"@types/tmp": "0.2.6", "@types/tmp": "0.2.6",
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",

View File

@ -4,11 +4,11 @@ import { examples, schemas } from "./resources"
import * as parameters from "./parameters" import * as parameters from "./parameters"
import * as security from "./security" import * as security from "./security"
const swaggerJsdoc = require("swagger-jsdoc") import swaggerJsdoc from "swagger-jsdoc"
const VARIABLES = {} const VARIABLES = {}
const options = { const opts: swaggerJsdoc.Options = {
definition: { definition: {
openapi: "3.0.0", openapi: "3.0.0",
info: { info: {
@ -58,7 +58,6 @@ const options = {
} }
function writeFile(output: any, filename: string) { function writeFile(output: any, filename: string) {
try {
const path = join(__dirname, filename) const path = join(__dirname, filename)
let spec = output let spec = output
if (filename.endsWith("json")) { if (filename.endsWith("json")) {
@ -71,17 +70,15 @@ function writeFile(output: any, filename: string) {
writeFileSync(path, spec) writeFileSync(path, spec)
console.log(`Wrote spec to ${path}`) console.log(`Wrote spec to ${path}`)
return path return path
} catch (err) {
console.error("Error writing spec file", err)
} }
export function spec() {
return swaggerJsdoc({ ...opts, format: ".json" })
} }
export function run() { export function run() {
const outputJSON = swaggerJsdoc(options) writeFile(swaggerJsdoc({ ...opts, format: ".json" }), "openapi.json")
options.format = ".yaml" return writeFile(swaggerJsdoc({ ...opts, format: ".yaml" }), "openapi.yaml")
const outputYAML = swaggerJsdoc(options)
writeFile(outputJSON, "openapi.json")
return writeFile(outputYAML, "openapi.yaml")
} }
if (require.main === module) { if (require.main === module) {

View File

@ -0,0 +1,21 @@
import { object } from "./utils"
import Resource from "./utils/Resource"
const errorSchema = object({
status: {
type: "number",
description: "The HTTP status code of the error.",
},
message: {
type: "string",
description: "A descriptive message about the error.",
},
})
export default new Resource()
.setExamples({
error: {},
})
.setSchemas({
error: errorSchema,
})

View File

@ -48,7 +48,7 @@ function getUser(ctx: UserCtx, userId?: string) {
if (userId) { if (userId) {
ctx.params = { userId } ctx.params = { userId }
} else if (!ctx.params?.userId) { } else if (!ctx.params?.userId) {
throw "No user ID provided for getting" throw new Error("No user ID provided for getting")
} }
return readGlobalUser(ctx) return readGlobalUser(ctx)
} }

View File

@ -12,6 +12,7 @@ import { paramResource, paramSubResource } from "../../../middleware/resourceId"
import { PermissionLevel, PermissionType } from "@budibase/types" import { PermissionLevel, PermissionType } from "@budibase/types"
import { CtxFn } from "./utils/Endpoint" import { CtxFn } from "./utils/Endpoint"
import mapperMiddleware from "./middleware/mapper" import mapperMiddleware from "./middleware/mapper"
import testErrorHandling from "./middleware/testErrorHandling"
import env from "../../../environment" import env from "../../../environment"
import { middleware, redis } from "@budibase/backend-core" import { middleware, redis } from "@budibase/backend-core"
import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils" import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils"
@ -144,6 +145,10 @@ function applyRoutes(
// add the output mapper middleware // add the output mapper middleware
addMiddleware(endpoints.read, mapperMiddleware, { output: true }) addMiddleware(endpoints.read, mapperMiddleware, { output: true })
addMiddleware(endpoints.write, mapperMiddleware, { output: true }) addMiddleware(endpoints.write, mapperMiddleware, { output: true })
if (env.isTest()) {
addMiddleware(endpoints.read, testErrorHandling())
addMiddleware(endpoints.write, testErrorHandling())
}
addToRouter(endpoints.read) addToRouter(endpoints.read)
addToRouter(endpoints.write) addToRouter(endpoints.write)
} }

View File

@ -0,0 +1,28 @@
import { Ctx } from "@budibase/types"
import environment from "../../../../environment"
export default () => {
if (!environment.isTest()) {
throw new Error("This middleware is only for testing")
}
return async (ctx: Ctx, next: any) => {
try {
await next()
} catch (err: any) {
if (!ctx.headers["x-budibase-include-stacktrace"]) {
throw err
}
const status = err.status || err.statusCode || 500
let error = err
while (error.cause) {
error = error.cause
}
ctx.status = status
ctx.body = { status, message: error.message, stack: error.stack }
}
}
}

View File

@ -2,11 +2,14 @@ import jestOpenAPI from "jest-openapi"
import { run as generateSchema } from "../../../../../specs/generate" import { run as generateSchema } from "../../../../../specs/generate"
import * as setup from "../../tests/utilities" import * as setup from "../../tests/utilities"
import { generateMakeRequest } from "./utils" import { generateMakeRequest } from "./utils"
import { Table, App, Row, User } from "@budibase/types" import { Table, App, Row } from "@budibase/types"
import nock from "nock"
import environment from "../../../../environment"
const yamlPath = generateSchema() const yamlPath = generateSchema()
jestOpenAPI(yamlPath!) jestOpenAPI(yamlPath!)
describe("compare", () => {
let config = setup.getConfig() let config = setup.getConfig()
let apiKey: string, table: Table, app: App, makeRequest: any let apiKey: string, table: Table, app: App, makeRequest: any
@ -19,6 +22,10 @@ beforeAll(async () => {
afterAll(setup.afterAll) afterAll(setup.afterAll)
beforeEach(() => {
nock.cleanAll()
})
describe("check the applications endpoints", () => { describe("check the applications endpoints", () => {
it("should allow retrieving applications through search", async () => { it("should allow retrieving applications through search", async () => {
const res = await makeRequest("post", "/applications/search") const res = await makeRequest("post", "/applications/search")
@ -58,6 +65,10 @@ describe("check the applications endpoints", () => {
}) })
it("should allow deleting an application", async () => { it("should allow deleting an application", async () => {
nock(environment.WORKER_URL!)
.delete(`/api/global/roles/${config.getProdAppId()}`)
.reply(200, {})
const res = await makeRequest( const res = await makeRequest(
"delete", "delete",
`/applications/${config.getAppId()}` `/applications/${config.getAppId()}`
@ -109,9 +120,13 @@ describe("check the rows endpoints", () => {
let row: Row let row: Row
it("should allow retrieving rows through search", async () => { it("should allow retrieving rows through search", async () => {
table = await config.upsertTable() table = await config.upsertTable()
const res = await makeRequest("post", `/tables/${table._id}/rows/search`, { const res = await makeRequest(
"post",
`/tables/${table._id}/rows/search`,
{
query: {}, query: {},
}) }
)
expect(res).toSatisfyApiSpec() expect(res).toSatisfyApiSpec()
}) })
@ -135,7 +150,10 @@ describe("check the rows endpoints", () => {
}) })
it("should allow retrieving a row", async () => { it("should allow retrieving a row", async () => {
const res = await makeRequest("get", `/tables/${table._id}/rows/${row._id}`) const res = await makeRequest(
"get",
`/tables/${table._id}/rows/${row._id}`
)
expect(res).toSatisfyApiSpec() expect(res).toSatisfyApiSpec()
}) })
@ -148,38 +166,10 @@ describe("check the rows endpoints", () => {
}) })
}) })
describe("check the users endpoints", () => {
let user: User
it("should allow retrieving users through search", async () => {
user = await config.createUser()
const res = await makeRequest("post", "/users/search")
expect(res).toSatisfyApiSpec()
})
it("should allow creating a user", async () => {
const res = await makeRequest("post", "/users")
expect(res).toSatisfyApiSpec()
})
it("should allow updating a user", async () => {
const res = await makeRequest("put", `/users/${user._id}`)
expect(res).toSatisfyApiSpec()
})
it("should allow retrieving a user", async () => {
const res = await makeRequest("get", `/users/${user._id}`)
expect(res).toSatisfyApiSpec()
})
it("should allow deleting a user", async () => {
const res = await makeRequest("delete", `/users/${user._id}`)
expect(res).toSatisfyApiSpec()
})
})
describe("check the queries endpoints", () => { describe("check the queries endpoints", () => {
it("should allow retrieving queries through search", async () => { it("should allow retrieving queries through search", async () => {
const res = await makeRequest("post", "/queries/search") const res = await makeRequest("post", "/queries/search")
expect(res).toSatisfyApiSpec() expect(res).toSatisfyApiSpec()
}) })
}) })
})

View File

@ -1,132 +1,143 @@
import * as setup from "../../tests/utilities" import * as setup from "../../tests/utilities"
import { generateMakeRequest, MakeRequestResponse } from "./utils"
import { User } from "@budibase/types" import { User } from "@budibase/types"
import { mocks } from "@budibase/backend-core/tests" import { generator, mocks } from "@budibase/backend-core/tests"
import nock from "nock"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
import { mockWorkerUserAPI } from "./utils"
import * as workerRequests from "../../../../utilities/workerRequests" describe("public users API", () => {
const config = new TestConfiguration()
const mockedWorkerReq = jest.mocked(workerRequests) let globalUser: User
let config = setup.getConfig()
let apiKey: string, globalUser: User, makeRequest: MakeRequestResponse
beforeAll(async () => { beforeAll(async () => {
await config.init() await config.init()
globalUser = await config.globalUser()
apiKey = await config.generateApiKey(globalUser._id)
makeRequest = generateMakeRequest(apiKey)
mockedWorkerReq.readGlobalUser.mockImplementation(() =>
Promise.resolve(globalUser)
)
}) })
afterAll(setup.afterAll) afterAll(setup.afterAll)
function base() { beforeEach(async () => {
return { globalUser = await config.globalUser()
tenantId: config.getTenantId(),
firstName: "Test",
lastName: "Test",
}
}
function updateMock() { nock.cleanAll()
mockedWorkerReq.readGlobalUser.mockImplementation(ctx => ctx.request.body) mockWorkerUserAPI(globalUser)
}
describe("check user endpoints", () => {
it("should not allow a user to update their own roles", async () => {
const res = await makeRequest("put", `/users/${globalUser._id}`, {
...globalUser,
roles: {
app_1: "ADMIN",
},
})
expect(
mockedWorkerReq.saveGlobalUser.mock.lastCall?.[0].body.data.roles["app_1"]
).toBeUndefined()
expect(res.status).toBe(200)
expect(res.body.data.roles["app_1"]).toBeUndefined()
}) })
it("should not allow a user to delete themselves", async () => { describe("read", () => {
const res = await makeRequest("delete", `/users/${globalUser._id}`) it("should allow a user to read themselves", async () => {
expect(res.status).toBe(405) const user = await config.api.user.find(globalUser._id!)
expect(mockedWorkerReq.deleteGlobalUser.mock.lastCall).toBeUndefined() expect(user._id).toBe(globalUser._id)
})
it("should allow a user to read another user", async () => {
const otherUser = await config.api.public.user.create({
email: generator.email({ domain: "example.com" }),
roles: {},
})
const user = await config.withUser(globalUser, () =>
config.api.public.user.find(otherUser._id!)
)
expect(user._id).toBe(otherUser._id)
}) })
}) })
describe("no user role update in free", () => { describe("create", () => {
beforeAll(() => { it("can successfully create a new user", async () => {
updateMock() const email = generator.email({ domain: "example.com" })
const newUser = await config.api.public.user.create({
email,
roles: {},
})
expect(newUser.email).toBe(email)
expect(newUser._id).toBeDefined()
}) })
describe("role creation on free tier", () => {
it("should not allow 'roles' to be updated", async () => { it("should not allow 'roles' to be updated", async () => {
const res = await makeRequest("post", "/users", { const newUser = await config.api.public.user.create({
...base(), email: generator.email({ domain: "example.com" }),
roles: { app_a: "BASIC" }, roles: { app_a: "BASIC" },
}) })
expect(res.status).toBe(200) expect(newUser.roles["app_a"]).toBeUndefined()
expect(res.body.data.roles["app_a"]).toBeUndefined()
expect(res.body.message).toBeDefined()
}) })
it("should not allow 'admin' to be updated", async () => { it("should not allow 'admin' to be updated", async () => {
const res = await makeRequest("post", "/users", { const newUser = await config.api.public.user.create({
...base(), email: generator.email({ domain: "example.com" }),
roles: {},
admin: { global: true }, admin: { global: true },
}) })
expect(res.status).toBe(200) expect(newUser.admin).toBeUndefined()
expect(res.body.data.admin).toBeUndefined()
expect(res.body.message).toBeDefined()
}) })
it("should not allow 'builder' to be updated", async () => { it("should not allow 'builder' to be updated", async () => {
const res = await makeRequest("post", "/users", { const newUser = await config.api.public.user.create({
...base(), email: generator.email({ domain: "example.com" }),
roles: {},
builder: { global: true }, builder: { global: true },
}) })
expect(res.status).toBe(200) expect(newUser.builder).toBeUndefined()
expect(res.body.data.builder).toBeUndefined()
expect(res.body.message).toBeDefined()
}) })
}) })
describe("no user role update in business", () => { describe("role creation on business tier", () => {
beforeAll(() => { beforeAll(() => {
updateMock()
mocks.licenses.useExpandedPublicApi() mocks.licenses.useExpandedPublicApi()
}) })
it("should allow 'roles' to be updated", async () => { it("should allow 'roles' to be updated", async () => {
const res = await makeRequest("post", "/users", { const newUser = await config.api.public.user.create({
...base(), email: generator.email({ domain: "example.com" }),
roles: { app_a: "BASIC" }, roles: { app_a: "BASIC" },
}) })
expect(res.status).toBe(200) expect(newUser.roles["app_a"]).toBe("BASIC")
expect(res.body.data.roles["app_a"]).toBe("BASIC")
expect(res.body.message).toBeUndefined()
}) })
it("should allow 'admin' to be updated", async () => { it("should allow 'admin' to be updated", async () => {
mocks.licenses.useExpandedPublicApi() const newUser = await config.api.public.user.create({
const res = await makeRequest("post", "/users", { email: generator.email({ domain: "example.com" }),
...base(), roles: {},
admin: { global: true }, admin: { global: true },
}) })
expect(res.status).toBe(200) expect(newUser.admin?.global).toBe(true)
expect(res.body.data.admin.global).toBe(true)
expect(res.body.message).toBeUndefined()
}) })
it("should allow 'builder' to be updated", async () => { it("should allow 'builder' to be updated", async () => {
mocks.licenses.useExpandedPublicApi() const newUser = await config.api.public.user.create({
const res = await makeRequest("post", "/users", { email: generator.email({ domain: "example.com" }),
...base(), roles: {},
builder: { global: true }, builder: { global: true },
}) })
expect(res.status).toBe(200) expect(newUser.builder?.global).toBe(true)
expect(res.body.data.builder.global).toBe(true) })
expect(res.body.message).toBeUndefined() })
})
describe("update", () => {
it("can update a user", async () => {
const updatedUser = await config.api.public.user.update({
...globalUser,
email: `updated-${globalUser.email}`,
})
expect(updatedUser.email).toBe(`updated-${globalUser.email}`)
})
it("should not allow a user to update their own roles", async () => {
await config.withUser(globalUser, () =>
config.api.public.user.update({
...globalUser,
roles: { app_1: "ADMIN" },
})
)
const updatedUser = await config.api.user.find(globalUser._id!)
expect(updatedUser.roles?.app_1).toBeUndefined()
})
})
describe("delete", () => {
it("should not allow a user to delete themselves", async () => {
await config.withUser(globalUser, () =>
config.api.public.user.destroy(globalUser._id!, { status: 405 })
)
})
}) })
}) })

View File

@ -1,6 +1,10 @@
import * as setup from "../../tests/utilities" import * as setup from "../../tests/utilities"
import { checkSlashesInUrl } from "../../../../utilities" import { checkSlashesInUrl } from "../../../../utilities"
import supertest from "supertest" import supertest from "supertest"
import { User } from "@budibase/types"
import environment from "../../../../environment"
import nock from "nock"
import { generator } from "@budibase/backend-core/tests"
export type HttpMethod = "post" | "get" | "put" | "delete" | "patch" export type HttpMethod = "post" | "get" | "put" | "delete" | "patch"
@ -91,3 +95,43 @@ export function generateMakeRequestWithFormData(
return res return res
} }
} }
export function mockWorkerUserAPI(...seedUsers: User[]) {
const users: Record<string, User> = {
...seedUsers.reduce((acc, user) => {
acc[user._id!] = user
return acc
}, {} as Record<string, User>),
}
nock(environment.WORKER_URL!)
.get(new RegExp(`/api/global/users/.*`))
.reply(200, (uri, body) => {
const id = uri.split("/").pop()
return users[id!]
})
.persist()
nock(environment.WORKER_URL!)
.post(`/api/global/users`)
.reply(200, (uri, body) => {
const newUser = body as User
if (!newUser._id) {
newUser._id = `us_${generator.guid()}`
}
users[newUser._id!] = newUser
return newUser
})
.persist()
nock(environment.WORKER_URL!)
.put(new RegExp(`/api/global/users/.*`))
.reply(200, (uri, body) => {
const id = uri.split("/").pop()!
const updatedUser = body as User
const existingUser = users[id] || {}
users[id] = { ...existingUser, ...updatedUser }
return users[id]
})
.persist()
}

View File

@ -3,44 +3,6 @@ import supertest from "supertest"
export * as structures from "../../../../tests/utilities/structures" export * as structures from "../../../../tests/utilities/structures"
function user() {
return {
_id: "user",
_rev: "rev",
createdAt: Date.now(),
email: "test@example.com",
roles: {},
tenantId: "default",
status: "active",
}
}
jest.mock("../../../../utilities/workerRequests", () => ({
getGlobalUsers: jest.fn(() => {
return {
_id: "us_uuid1",
}
}),
getGlobalSelf: jest.fn(() => {
return {
_id: "us_uuid1",
}
}),
allGlobalUsers: jest.fn(() => {
return [user()]
}),
readGlobalUser: jest.fn(() => {
return user()
}),
saveGlobalUser: jest.fn(() => {
return { _id: "user", _rev: "rev" }
}),
deleteGlobalUser: jest.fn(() => {
return { message: "deleted user" }
}),
removeAppFromUserRoles: jest.fn(),
}))
export function delay(ms: number) { export function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)) return new Promise(resolve => setTimeout(resolve, ms))
} }

View File

@ -67,6 +67,7 @@ import {
View, View,
Webhook, Webhook,
WithRequired, WithRequired,
DevInfo,
} from "@budibase/types" } from "@budibase/types"
import API from "./api" import API from "./api"
@ -248,7 +249,7 @@ export default class TestConfiguration {
} }
} }
async withUser(user: User, f: () => Promise<void>) { async withUser<T>(user: User, f: () => Promise<T>): Promise<T> {
const oldUser = this.user const oldUser = this.user
this.user = user this.user = user
try { try {
@ -469,7 +470,10 @@ export default class TestConfiguration {
} }
} }
defaultHeaders(extras = {}, prodApp = false) { defaultHeaders(
extras: Record<string, string | string[]> = {},
prodApp = false
) {
const tenantId = this.getTenantId() const tenantId = this.getTenantId()
const user = this.getUser() const user = this.getUser()
const authObj: AuthToken = { const authObj: AuthToken = {
@ -498,10 +502,13 @@ export default class TestConfiguration {
} }
} }
publicHeaders({ prodApp = true } = {}) { publicHeaders({
prodApp = true,
extras = {},
}: { prodApp?: boolean; extras?: Record<string, string | string[]> } = {}) {
const appId = prodApp ? this.prodAppId : this.appId const appId = prodApp ? this.prodAppId : this.appId
const headers: any = { const headers: Record<string, string> = {
Accept: "application/json", Accept: "application/json",
Cookie: "", Cookie: "",
} }
@ -514,6 +521,7 @@ export default class TestConfiguration {
return { return {
...headers, ...headers,
...this.temporaryHeaders, ...this.temporaryHeaders,
...extras,
} }
} }
@ -577,17 +585,17 @@ export default class TestConfiguration {
} }
const db = tenancy.getTenantDB(this.getTenantId()) const db = tenancy.getTenantDB(this.getTenantId())
const id = dbCore.generateDevInfoID(userId) const id = dbCore.generateDevInfoID(userId)
let devInfo: any const devInfo = await db.tryGet<DevInfo>(id)
try { if (devInfo && devInfo.apiKey) {
devInfo = await db.get(id) return devInfo.apiKey
} catch (err) {
devInfo = { _id: id, userId }
} }
devInfo.apiKey = encryption.encrypt(
const apiKey = encryption.encrypt(
`${this.getTenantId()}${dbCore.SEPARATOR}${newid()}` `${this.getTenantId()}${dbCore.SEPARATOR}${newid()}`
) )
await db.put(devInfo) const newDevInfo: DevInfo = { _id: id, userId, apiKey }
return devInfo.apiKey await db.put(newDevInfo)
return apiKey
} }
// APP // APP

View File

@ -1,8 +1,12 @@
import jestOpenAPI from "jest-openapi"
import { spec } from "../../../../specs/generate"
import TestConfiguration from "../TestConfiguration" import TestConfiguration from "../TestConfiguration"
import request, { SuperTest, Test, Response } from "supertest" import request, { SuperTest, Test, Response } from "supertest"
import { ReadStream } from "fs" import { ReadStream } from "fs"
import { getServer } from "../../../app" import { getServer } from "../../../app"
jestOpenAPI(spec() as any)
type Headers = Record<string, string | string[] | undefined> type Headers = Record<string, string | string[] | undefined>
type Method = "get" | "post" | "put" | "patch" | "delete" type Method = "get" | "post" | "put" | "patch" | "delete"
@ -46,6 +50,7 @@ export interface RequestOpts {
export abstract class TestAPI { export abstract class TestAPI {
config: TestConfiguration config: TestConfiguration
request: SuperTest<Test> request: SuperTest<Test>
prefix = ""
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.config = config this.config = config
@ -53,26 +58,26 @@ export abstract class TestAPI {
} }
protected _get = async <T>(url: string, opts?: RequestOpts): Promise<T> => { protected _get = async <T>(url: string, opts?: RequestOpts): Promise<T> => {
return await this._request<T>("get", url, opts) return await this._request<T>("get", `${this.prefix}${url}`, opts)
} }
protected _post = async <T>(url: string, opts?: RequestOpts): Promise<T> => { protected _post = async <T>(url: string, opts?: RequestOpts): Promise<T> => {
return await this._request<T>("post", url, opts) return await this._request<T>("post", `${this.prefix}${url}`, opts)
} }
protected _put = async <T>(url: string, opts?: RequestOpts): Promise<T> => { protected _put = async <T>(url: string, opts?: RequestOpts): Promise<T> => {
return await this._request<T>("put", url, opts) return await this._request<T>("put", `${this.prefix}${url}`, opts)
} }
protected _patch = async <T>(url: string, opts?: RequestOpts): Promise<T> => { protected _patch = async <T>(url: string, opts?: RequestOpts): Promise<T> => {
return await this._request<T>("patch", url, opts) return await this._request<T>("patch", `${this.prefix}${url}`, opts)
} }
protected _delete = async <T>( protected _delete = async <T>(
url: string, url: string,
opts?: RequestOpts opts?: RequestOpts
): Promise<T> => { ): Promise<T> => {
return await this._request<T>("delete", url, opts) return await this._request<T>("delete", `${this.prefix}${url}`, opts)
} }
protected _requestRaw = async ( protected _requestRaw = async (
@ -88,7 +93,6 @@ export abstract class TestAPI {
fields = {}, fields = {},
files = {}, files = {},
expectations, expectations,
publicUser = false,
} = opts || {} } = opts || {}
const { status = 200 } = expectations || {} const { status = 200 } = expectations || {}
const expectHeaders = expectations?.headers || {} const expectHeaders = expectations?.headers || {}
@ -97,7 +101,7 @@ export abstract class TestAPI {
expectHeaders["Content-Type"] = /^application\/json/ expectHeaders["Content-Type"] = /^application\/json/
} }
let queryParams = [] let queryParams: string[] = []
for (const [key, value] of Object.entries(query)) { for (const [key, value] of Object.entries(query)) {
if (value) { if (value) {
queryParams.push(`${key}=${value}`) queryParams.push(`${key}=${value}`)
@ -107,18 +111,10 @@ export abstract class TestAPI {
url += `?${queryParams.join("&")}` url += `?${queryParams.join("&")}`
} }
const headersFn = publicUser
? (_extras = {}) =>
this.config.publicHeaders.bind(this.config)({
prodApp: opts?.useProdApp,
})
: (extras = {}) =>
this.config.defaultHeaders.bind(this.config)(extras, opts?.useProdApp)
const app = getServer() const app = getServer()
let req = request(app)[method](url) let req = request(app)[method](url)
req = req.set( req = req.set(
headersFn({ await this.getHeaders(opts, {
"x-budibase-include-stacktrace": "true", "x-budibase-include-stacktrace": "true",
}) })
) )
@ -167,10 +163,18 @@ export abstract class TestAPI {
} }
} }
protected _checkResponse = ( protected async getHeaders(
response: Response, opts?: RequestOpts,
expectations?: Expectations extras?: Record<string, string | string[]>
) => { ): Promise<Record<string, string | string[]>> {
if (opts?.publicUser) {
return this.config.publicHeaders({ prodApp: opts?.useProdApp, extras })
} else {
return this.config.defaultHeaders(extras, opts?.useProdApp)
}
}
protected _checkResponse(response: Response, expectations?: Expectations) {
const { status = 200 } = expectations || {} const { status = 200 } = expectations || {}
if (response.status !== status) { if (response.status !== status) {
@ -236,3 +240,34 @@ export abstract class TestAPI {
).body ).body
} }
} }
export abstract class PublicAPI extends TestAPI {
prefix = "/api/public/v1"
protected async getHeaders(
opts?: RequestOpts,
extras?: Record<string, string | string[]>
): Promise<Record<string, string | string[]>> {
const apiKey = await this.config.generateApiKey()
const headers: Record<string, string | string[]> = {
Accept: "application/json",
Host: this.config.tenantHost(),
"x-budibase-api-key": apiKey,
"x-budibase-app-id": this.config.getAppId(),
...extras,
}
return headers
}
protected _checkResponse(response: Response, expectations?: Expectations) {
const checked = super._checkResponse(response, expectations)
if (checked.status >= 200 && checked.status < 300) {
// We don't seem to have documented our errors yet, so for the time being
// we'll only do the schema check for successful responses.
expect(checked).toSatisfyApiSpec()
}
return checked
}
}

View File

@ -17,6 +17,7 @@ import { RowActionAPI } from "./rowAction"
import { AutomationAPI } from "./automation" import { AutomationAPI } from "./automation"
import { PluginAPI } from "./plugin" import { PluginAPI } from "./plugin"
import { WebhookAPI } from "./webhook" import { WebhookAPI } from "./webhook"
import { UserPublicAPI } from "./public/user"
export default class API { export default class API {
application: ApplicationAPI application: ApplicationAPI
@ -38,6 +39,10 @@ export default class API {
viewV2: ViewV2API viewV2: ViewV2API
webhook: WebhookAPI webhook: WebhookAPI
public: {
user: UserPublicAPI
}
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.application = new ApplicationAPI(config) this.application = new ApplicationAPI(config)
this.attachment = new AttachmentAPI(config) this.attachment = new AttachmentAPI(config)
@ -57,5 +62,8 @@ export default class API {
this.user = new UserAPI(config) this.user = new UserAPI(config)
this.viewV2 = new ViewV2API(config) this.viewV2 = new ViewV2API(config)
this.webhook = new WebhookAPI(config) this.webhook = new WebhookAPI(config)
this.public = {
user: new UserPublicAPI(config),
}
} }
} }

View File

@ -0,0 +1,34 @@
import { UnsavedUser, User } from "@budibase/types"
import { Expectations, PublicAPI } from "../base"
export class UserPublicAPI extends PublicAPI {
find = async (id: string, expectations?: Expectations): Promise<User> => {
const response = await this._get<{ data: User }>(`/users/${id}`, {
expectations,
})
return response.data
}
update = async (user: User, expectations?: Expectations): Promise<User> => {
const response = await this._put<{ data: User }>(`/users/${user._id}`, {
body: user,
expectations,
})
return response.data
}
destroy = async (id: string, expectations?: Expectations): Promise<void> => {
return await this._delete(`/users/${id}`, { expectations })
}
create = async (
user: UnsavedUser,
expectations?: Expectations
): Promise<User> => {
const response = await this._post<{ data: User }>("/users", {
body: user,
expectations,
})
return response.data
}
}

View File

@ -2796,9 +2796,9 @@
through2 "^2.0.0" through2 "^2.0.0"
"@budibase/pro@npm:@budibase/pro@latest": "@budibase/pro@npm:@budibase/pro@latest":
version "3.4.20" version "3.4.22"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.4.20.tgz#0d855d6ed8fe92fd178c74a8963d879cc124b034" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.4.22.tgz#943f23cb7056041bc1f433ee60b3d093145e7a4a"
integrity sha512-hUteGvhMOKjBo0fluxcqNs7d4x8OU5W8Oqqrm7eIS9Ohe7ala2iWNCcrj+x+S9CavIm6s7JZZnAewa2Maiz2zQ== integrity sha512-Du3iZsmRLopfoi2SvxQyY1P2Su3Nw0WbITOrKmZFsVLjZ9MzzTZs0Ph/SJHzrfJpM7rn9+8788BLSf3Z3l9KcQ==
dependencies: dependencies:
"@anthropic-ai/sdk" "^0.27.3" "@anthropic-ai/sdk" "^0.27.3"
"@budibase/backend-core" "*" "@budibase/backend-core" "*"
@ -7142,6 +7142,11 @@
dependencies: dependencies:
"@types/superagent" "*" "@types/superagent" "*"
"@types/swagger-jsdoc@^6.0.4":
version "6.0.4"
resolved "https://registry.yarnpkg.com/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz#bb4f60f3a5f103818e022f2e29ff8935113fb83d"
integrity sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==
"@types/tar-fs@2.0.1": "@types/tar-fs@2.0.1":
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.1.tgz#6391dcad1b03dea2d79fac07371585ab54472bb1" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.1.tgz#6391dcad1b03dea2d79fac07371585ab54472bb1"