This commit is contained in:
Sam Rose 2025-02-25 18:23:29 +00:00
parent e69bfd7eb5
commit 433c20d80c
No known key found for this signature in database
6 changed files with 129 additions and 103 deletions

View File

@ -1,27 +1,44 @@
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 { mocks } from "@budibase/backend-core/tests"
import nock from "nock"
import environment from "../../../../environment"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
import * as workerRequests from "../../../../utilities/workerRequests" const config = new TestConfiguration()
let globalUser: User
const mockedWorkerReq = jest.mocked(workerRequests)
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)
beforeEach(async () => {
globalUser = await config.globalUser()
nock.cleanAll()
nock(environment.WORKER_URL!)
.get(`/api/global/users/${globalUser._id}`)
.reply(200, (uri, body) => {
return globalUser
})
.persist()
nock(environment.WORKER_URL!)
.post(`/api/global/users`)
.reply(200, (uri, body) => {
const updatedUser = body as User
if (updatedUser._id === globalUser._id) {
globalUser = updatedUser
return globalUser
} else {
throw new Error("User not found")
}
})
.persist()
})
function base() { function base() {
return { return {
tenantId: config.getTenantId(), tenantId: config.getTenantId(),
@ -30,37 +47,26 @@ function base() {
} }
} }
function updateMock() { describe.only("check user endpoints", () => {
mockedWorkerReq.readGlobalUser.mockImplementation(ctx => ctx.request.body)
}
describe("check user endpoints", () => {
it("should not allow a user to update their own roles", async () => { it("should not allow a user to update their own roles", async () => {
const res = await makeRequest("put", `/users/${globalUser._id}`, { await config.withUser(globalUser, () =>
config.api.public.user.update({
...globalUser, ...globalUser,
roles: { roles: { app_1: "ADMIN" },
app_1: "ADMIN",
},
}) })
expect( )
mockedWorkerReq.saveGlobalUser.mock.lastCall?.[0].body.data.roles["app_1"] const updatedUser = await config.api.user.find(globalUser._id!)
).toBeUndefined() expect(updatedUser.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 () => { it("should not allow a user to delete themselves", async () => {
const res = await makeRequest("delete", `/users/${globalUser._id}`) await config.withUser(globalUser, () =>
expect(res.status).toBe(405) config.api.public.user.destroy(globalUser._id!, { status: 405 })
expect(mockedWorkerReq.deleteGlobalUser.mock.lastCall).toBeUndefined() )
}) })
}) })
describe("no user role update in free", () => { describe("no user role update in free", () => {
beforeAll(() => {
updateMock()
})
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 res = await makeRequest("post", "/users", {
...base(), ...base(),
@ -94,7 +100,6 @@ describe("no user role update in free", () => {
describe("no user role update in business", () => { describe("no user role update in business", () => {
beforeAll(() => { beforeAll(() => {
updateMock()
mocks.licenses.useExpandedPublicApi() mocks.licenses.useExpandedPublicApi()
}) })

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

@ -46,6 +46,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 +54,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 +89,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 +97,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 +107,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,6 +159,17 @@ export abstract class TestAPI {
} }
} }
protected async getHeaders(
opts?: RequestOpts,
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 = ( protected _checkResponse = (
response: Response, response: Response,
expectations?: Expectations expectations?: Expectations
@ -236,3 +239,24 @@ 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
}
}

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,19 @@
import { User } from "@budibase/types"
import { Expectations, PublicAPI } from "../base"
export class UserPublicAPI extends PublicAPI {
find = async (id: string, expectations?: Expectations): Promise<User> => {
return await this._get<User>(`/users/${id}`, { expectations })
}
update = async (user: User, expectations?: Expectations): Promise<User> => {
return await this._put<User>(`/users/${user._id}`, {
body: user,
expectations,
})
}
destroy = async (id: string, expectations?: Expectations): Promise<void> => {
return await this._delete(`/users/${id}`, { expectations })
}
}