diff --git a/packages/server/specs/resources/error.ts b/packages/server/specs/resources/error.ts new file mode 100644 index 0000000000..0d42963b73 --- /dev/null +++ b/packages/server/specs/resources/error.ts @@ -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, + }) diff --git a/packages/server/src/api/routes/public/tests/compare.spec.ts b/packages/server/src/api/routes/public/tests/compare.spec.ts index ccf248f360..b4f5f20e2c 100644 --- a/packages/server/src/api/routes/public/tests/compare.spec.ts +++ b/packages/server/src/api/routes/public/tests/compare.spec.ts @@ -2,184 +2,174 @@ import jestOpenAPI from "jest-openapi" import { run as generateSchema } from "../../../../../specs/generate" import * as setup from "../../tests/utilities" 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() jestOpenAPI(yamlPath!) -let config = setup.getConfig() -let apiKey: string, table: Table, app: App, makeRequest: any +describe("compare", () => { + let config = setup.getConfig() + let apiKey: string, table: Table, app: App, makeRequest: any -beforeAll(async () => { - app = await config.init() - table = await config.upsertTable() - apiKey = await config.generateApiKey() - makeRequest = generateMakeRequest(apiKey) -}) - -afterAll(setup.afterAll) - -describe("check the applications endpoints", () => { - it("should allow retrieving applications through search", async () => { - const res = await makeRequest("post", "/applications/search") - expect(res).toSatisfyApiSpec() - }) - - it("should allow creating an application", async () => { - const res = await makeRequest( - "post", - "/applications", - { - name: "new App", - }, - null - ) - expect(res).toSatisfyApiSpec() - }) - - it("should allow updating an application", async () => { - const app = config.getApp() - const appId = config.getAppId() - const res = await makeRequest( - "put", - `/applications/${appId}`, - { - ...app, - name: "updated app name", - }, - appId - ) - expect(res).toSatisfyApiSpec() - }) - - it("should allow retrieving an application", async () => { - const res = await makeRequest("get", `/applications/${config.getAppId()}`) - expect(res).toSatisfyApiSpec() - }) - - it("should allow deleting an application", async () => { - const res = await makeRequest( - "delete", - `/applications/${config.getAppId()}` - ) - expect(res).toSatisfyApiSpec() - }) -}) - -describe("check the tables endpoints", () => { - it("should allow retrieving tables through search", async () => { - await config.createApp("new app 1") + beforeAll(async () => { + app = await config.init() table = await config.upsertTable() - const res = await makeRequest("post", "/tables/search") - expect(res).toSatisfyApiSpec() + apiKey = await config.generateApiKey() + makeRequest = generateMakeRequest(apiKey) }) - it("should allow creating a table", async () => { - const res = await makeRequest("post", "/tables", { - name: "table name", - primaryDisplay: "column1", - schema: { - column1: { - type: "string", - constraints: {}, + afterAll(setup.afterAll) + + beforeEach(() => { + nock.cleanAll() + }) + + describe("check the applications endpoints", () => { + it("should allow retrieving applications through search", async () => { + const res = await makeRequest("post", "/applications/search") + expect(res).toSatisfyApiSpec() + }) + + it("should allow creating an application", async () => { + const res = await makeRequest( + "post", + "/applications", + { + name: "new App", }, - }, + null + ) + expect(res).toSatisfyApiSpec() }) - expect(res).toSatisfyApiSpec() - }) - it("should allow updating a table", async () => { - const updated = { ...table, _rev: undefined, name: "new name" } - const res = await makeRequest("put", `/tables/${table._id}`, updated) - expect(res).toSatisfyApiSpec() - }) - - it("should allow retrieving a table", async () => { - const res = await makeRequest("get", `/tables/${table._id}`) - expect(res).toSatisfyApiSpec() - }) - - it("should allow deleting a table", async () => { - const res = await makeRequest("delete", `/tables/${table._id}`) - expect(res).toSatisfyApiSpec() - }) -}) - -describe("check the rows endpoints", () => { - let row: Row - it("should allow retrieving rows through search", async () => { - table = await config.upsertTable() - const res = await makeRequest("post", `/tables/${table._id}/rows/search`, { - query: {}, + it("should allow updating an application", async () => { + const app = config.getApp() + const appId = config.getAppId() + const res = await makeRequest( + "put", + `/applications/${appId}`, + { + ...app, + name: "updated app name", + }, + appId + ) + expect(res).toSatisfyApiSpec() }) - expect(res).toSatisfyApiSpec() - }) - it("should allow creating a row", async () => { - const res = await makeRequest("post", `/tables/${table._id}/rows`, { - name: "test row", + it("should allow retrieving an application", async () => { + const res = await makeRequest("get", `/applications/${config.getAppId()}`) + expect(res).toSatisfyApiSpec() + }) + + it("should allow deleting an application", async () => { + nock(environment.WORKER_URL!) + .delete(`/api/global/roles/${config.getProdAppId()}`) + .reply(200, {}) + + const res = await makeRequest( + "delete", + `/applications/${config.getAppId()}` + ) + expect(res).toSatisfyApiSpec() }) - expect(res).toSatisfyApiSpec() - row = res.body.data }) - it("should allow updating a row", async () => { - const res = await makeRequest( - "put", - `/tables/${table._id}/rows/${row._id}`, - { - name: "test row updated", - } - ) - expect(res).toSatisfyApiSpec() + describe("check the tables endpoints", () => { + it("should allow retrieving tables through search", async () => { + await config.createApp("new app 1") + table = await config.upsertTable() + const res = await makeRequest("post", "/tables/search") + expect(res).toSatisfyApiSpec() + }) + + it("should allow creating a table", async () => { + const res = await makeRequest("post", "/tables", { + name: "table name", + primaryDisplay: "column1", + schema: { + column1: { + type: "string", + constraints: {}, + }, + }, + }) + expect(res).toSatisfyApiSpec() + }) + + it("should allow updating a table", async () => { + const updated = { ...table, _rev: undefined, name: "new name" } + const res = await makeRequest("put", `/tables/${table._id}`, updated) + expect(res).toSatisfyApiSpec() + }) + + it("should allow retrieving a table", async () => { + const res = await makeRequest("get", `/tables/${table._id}`) + expect(res).toSatisfyApiSpec() + }) + + it("should allow deleting a table", async () => { + const res = await makeRequest("delete", `/tables/${table._id}`) + expect(res).toSatisfyApiSpec() + }) }) - it("should allow retrieving a row", async () => { - const res = await makeRequest("get", `/tables/${table._id}/rows/${row._id}`) - expect(res).toSatisfyApiSpec() + describe("check the rows endpoints", () => { + let row: Row + it("should allow retrieving rows through search", async () => { + table = await config.upsertTable() + const res = await makeRequest( + "post", + `/tables/${table._id}/rows/search`, + { + query: {}, + } + ) + expect(res).toSatisfyApiSpec() + }) + + it("should allow creating a row", async () => { + const res = await makeRequest("post", `/tables/${table._id}/rows`, { + name: "test row", + }) + expect(res).toSatisfyApiSpec() + row = res.body.data + }) + + it("should allow updating a row", async () => { + const res = await makeRequest( + "put", + `/tables/${table._id}/rows/${row._id}`, + { + name: "test row updated", + } + ) + expect(res).toSatisfyApiSpec() + }) + + it("should allow retrieving a row", async () => { + const res = await makeRequest( + "get", + `/tables/${table._id}/rows/${row._id}` + ) + expect(res).toSatisfyApiSpec() + }) + + it("should allow deleting a row", async () => { + const res = await makeRequest( + "delete", + `/tables/${table._id}/rows/${row._id}` + ) + expect(res).toSatisfyApiSpec() + }) }) - it("should allow deleting a row", async () => { - const res = await makeRequest( - "delete", - `/tables/${table._id}/rows/${row._id}` - ) - expect(res).toSatisfyApiSpec() - }) -}) - -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", () => { - it("should allow retrieving queries through search", async () => { - const res = await makeRequest("post", "/queries/search") - expect(res).toSatisfyApiSpec() + describe("check the queries endpoints", () => { + it("should allow retrieving queries through search", async () => { + const res = await makeRequest("post", "/queries/search") + expect(res).toSatisfyApiSpec() + }) }) }) diff --git a/packages/server/src/api/routes/public/tests/users.spec.ts b/packages/server/src/api/routes/public/tests/users.spec.ts index 13630e443d..bb3e4b6fba 100644 --- a/packages/server/src/api/routes/public/tests/users.spec.ts +++ b/packages/server/src/api/routes/public/tests/users.spec.ts @@ -2,120 +2,142 @@ import * as setup from "../../tests/utilities" import { User } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" import nock from "nock" -import environment from "../../../../environment" import TestConfiguration from "../../../../tests/utilities/TestConfiguration" +import { mockWorkerUserAPI } from "./utils" -const config = new TestConfiguration() -let globalUser: User -let users: Record = {} +describe("public users API", () => { + const config = new TestConfiguration() + let globalUser: User -beforeAll(async () => { - await config.init() -}) + beforeAll(async () => { + await config.init() + }) -afterAll(setup.afterAll) + afterAll(setup.afterAll) -beforeEach(async () => { - globalUser = await config.globalUser() - users[globalUser._id!] = globalUser + beforeEach(async () => { + globalUser = await config.globalUser() - nock.cleanAll() - nock(environment.WORKER_URL!) - .get(new RegExp(`/api/global/users/.*`)) - .reply(200, (uri, body) => { - const id = uri.split("/").pop() - return users[id!] + nock.cleanAll() + mockWorkerUserAPI(globalUser) + }) + + describe("read", () => { + it("should allow a user to read themselves", async () => { + const user = await config.api.user.find(globalUser._id!) + expect(user._id).toBe(globalUser._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() -}) - -describe("check user endpoints", () => { - 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" }, + 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 updatedUser = await config.api.user.find(globalUser._id!) - expect(updatedUser.roles?.app_1).toBeUndefined() + const user = await config.withUser(globalUser, () => + config.api.public.user.find(otherUser._id!) + ) + expect(user._id).toBe(otherUser._id) + }) }) - it("should not allow a user to delete themselves", async () => { - await config.withUser(globalUser, () => - config.api.public.user.destroy(globalUser._id!, { status: 405 }) - ) - }) -}) - -describe("role updating on free tier", () => { - it("should not allow 'roles' to be updated", async () => { - const newUser = await config.api.public.user.create({ - email: generator.email({ domain: "example.com" }), - roles: { app_a: "BASIC" }, - }) - expect(newUser.roles["app_a"]).toBeUndefined() - }) - - it("should not allow 'admin' to be updated", async () => { - const newUser = await config.api.public.user.create({ - email: generator.email({ domain: "example.com" }), - roles: {}, - admin: { global: true }, - }) - expect(newUser.admin).toBeUndefined() - }) - - it("should not allow 'builder' to be updated", async () => { - const newUser = await config.api.public.user.create({ - email: generator.email({ domain: "example.com" }), - roles: {}, - builder: { global: true }, - }) - expect(newUser.builder).toBeUndefined() - }) -}) - -describe("role updating on business tier", () => { - beforeAll(() => { - mocks.licenses.useExpandedPublicApi() - }) - - it("should allow 'roles' to be updated", async () => { - const newUser = await config.api.public.user.create({ - email: generator.email({ domain: "example.com" }), - roles: { app_a: "BASIC" }, - }) - expect(newUser.roles["app_a"]).toBe("BASIC") - }) - - it("should allow 'admin' to be updated", async () => { - const newUser = await config.api.public.user.create({ - email: generator.email({ domain: "example.com" }), - roles: {}, - admin: { global: true }, - }) - expect(newUser.admin?.global).toBe(true) - }) - - it("should allow 'builder' to be updated", async () => { - const newUser = await config.api.public.user.create({ - email: generator.email({ domain: "example.com" }), - roles: {}, - builder: { global: true }, - }) - expect(newUser.builder?.global).toBe(true) + describe("create", () => { + it("can successfully create a new user", async () => { + 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 () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: { app_a: "BASIC" }, + }) + expect(newUser.roles["app_a"]).toBeUndefined() + }) + + it("should not allow 'admin' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + admin: { global: true }, + }) + expect(newUser.admin).toBeUndefined() + }) + + it("should not allow 'builder' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + builder: { global: true }, + }) + expect(newUser.builder).toBeUndefined() + }) + }) + + describe("role creation on business tier", () => { + beforeAll(() => { + mocks.licenses.useExpandedPublicApi() + }) + + it("should allow 'roles' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: { app_a: "BASIC" }, + }) + expect(newUser.roles["app_a"]).toBe("BASIC") + }) + + it("should allow 'admin' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + admin: { global: true }, + }) + expect(newUser.admin?.global).toBe(true) + }) + + it("should allow 'builder' to be updated", async () => { + const newUser = await config.api.public.user.create({ + email: generator.email({ domain: "example.com" }), + roles: {}, + builder: { global: true }, + }) + expect(newUser.builder?.global).toBe(true) + }) + }) + }) + + 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 }) + ) + }) }) }) diff --git a/packages/server/src/api/routes/public/tests/utils.ts b/packages/server/src/api/routes/public/tests/utils.ts index 4fb048a540..4985c0db58 100644 --- a/packages/server/src/api/routes/public/tests/utils.ts +++ b/packages/server/src/api/routes/public/tests/utils.ts @@ -1,6 +1,10 @@ import * as setup from "../../tests/utilities" import { checkSlashesInUrl } from "../../../../utilities" 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" @@ -91,3 +95,43 @@ export function generateMakeRequestWithFormData( return res } } + +export function mockWorkerUserAPI(...seedUsers: User[]) { + const users: Record = { + ...seedUsers.reduce((acc, user) => { + acc[user._id!] = user + return acc + }, {} as Record), + } + + 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() +} diff --git a/packages/server/src/tests/utilities/api/base.ts b/packages/server/src/tests/utilities/api/base.ts index 177c3c3c0b..2b3e3db44c 100644 --- a/packages/server/src/tests/utilities/api/base.ts +++ b/packages/server/src/tests/utilities/api/base.ts @@ -1,8 +1,13 @@ +import jestOpenAPI from "jest-openapi" +import { run as generateSchema } from "../../../../specs/generate" import TestConfiguration from "../TestConfiguration" import request, { SuperTest, Test, Response } from "supertest" import { ReadStream } from "fs" import { getServer } from "../../../app" +const yamlPath = generateSchema() +jestOpenAPI(yamlPath!) + type Headers = Record type Method = "get" | "post" | "put" | "patch" | "delete" @@ -170,10 +175,7 @@ export abstract class TestAPI { } } - protected _checkResponse = ( - response: Response, - expectations?: Expectations - ) => { + protected _checkResponse(response: Response, expectations?: Expectations) { const { status = 200 } = expectations || {} if (response.status !== status) { @@ -259,4 +261,14 @@ export abstract class PublicAPI extends TestAPI { 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 + } } diff --git a/packages/server/src/tests/utilities/api/public/user.ts b/packages/server/src/tests/utilities/api/public/user.ts index a8a7baed7f..4f5fccc740 100644 --- a/packages/server/src/tests/utilities/api/public/user.ts +++ b/packages/server/src/tests/utilities/api/public/user.ts @@ -3,14 +3,18 @@ import { Expectations, PublicAPI } from "../base" export class UserPublicAPI extends PublicAPI { find = async (id: string, expectations?: Expectations): Promise => { - return await this._get(`/users/${id}`, { expectations }) + const response = await this._get<{ data: User }>(`/users/${id}`, { + expectations, + }) + return response.data } update = async (user: User, expectations?: Expectations): Promise => { - return await this._put(`/users/${user._id}`, { + const response = await this._put<{ data: User }>(`/users/${user._id}`, { body: user, expectations, }) + return response.data } destroy = async (id: string, expectations?: Expectations): Promise => {