budibase/packages/server/src/tests/utilities/TestConfiguration.ts

741 lines
17 KiB
TypeScript
Raw Normal View History

2023-02-03 12:28:27 +01:00
import { generator, mocks, structures } from "@budibase/backend-core/tests"
// init the licensing mock
import * as pro from "@budibase/pro"
mocks.licenses.init(pro)
// use unlimited license by default
mocks.licenses.useUnlimited()
import { init as dbInit } from "../../db"
dbInit()
import env from "../../environment"
2023-01-31 18:27:25 +01:00
import { env as coreEnv } from "@budibase/backend-core"
import {
2021-03-03 19:41:49 +01:00
basicTable,
basicRow,
basicRole,
basicAutomation,
2021-03-04 11:05:50 +01:00
basicDatasource,
basicQuery,
2021-03-08 15:49:19 +01:00
basicScreen,
basicLayout,
basicWebhook,
} from "./structures"
import {
constants,
tenancy,
sessions,
cache,
context,
db as dbCore,
encryption,
auth,
roles,
} from "@budibase/backend-core"
import * as controllers from "./controllers"
import { cleanup } from "../../utilities/fileSystem"
import newid from "../../db/newid"
import { generateUserMetadataID } from "../../db/utils"
import { startup } from "../../startup"
2023-01-16 16:35:41 +01:00
import supertest from "supertest"
import {
App,
AuthToken,
Datasource,
Row,
SourceName,
Table,
2023-03-21 18:27:31 +01:00
SearchFilters,
2023-03-29 17:42:55 +02:00
UserRoles,
} from "@budibase/types"
2023-01-25 20:01:24 +01:00
type DefaultUserValues = {
globalUserId: string
email: string
firstName: string
lastName: string
csrfToken: string
}
class TestConfiguration {
server: any
2023-01-16 16:35:41 +01:00
request: supertest.SuperTest<supertest.Test> | undefined
started: boolean
appId: string | null
allApps: any[]
app?: App
prodApp: any
prodAppId: any
user: any
globalUserId: any
userMetadataId: any
table: any
linkedTable: any
automation: any
datasource: any
tenantId?: string
2023-01-25 20:01:24 +01:00
defaultUserValues: DefaultUserValues
constructor(openServer = true) {
if (openServer) {
// use a random port because it doesn't matter
env.PORT = "0"
this.server = require("../../app").default
// we need the request for logging in, involves cookies, hard to fake
this.request = supertest(this.server)
this.started = true
} else {
this.started = false
}
this.appId = null
this.allApps = []
2023-01-25 20:01:24 +01:00
this.defaultUserValues = this.populateDefaultUserValues()
2023-01-25 18:11:37 +01:00
}
2023-01-25 20:01:24 +01:00
populateDefaultUserValues(): DefaultUserValues {
2023-01-25 18:11:37 +01:00
return {
2023-02-01 18:11:50 +01:00
globalUserId: `us_${newid()}`,
email: generator.email(),
firstName: generator.first(),
lastName: generator.last(),
csrfToken: generator.hash(),
2023-01-25 18:11:37 +01:00
}
}
getRequest() {
return this.request
}
2022-02-25 16:55:19 +01:00
getApp() {
return this.app
}
2022-08-10 12:01:54 +02:00
getProdApp() {
return this.prodApp
}
getAppId() {
return this.appId
}
2022-04-04 16:59:00 +02:00
getProdAppId() {
return this.prodAppId
}
getUserDetails() {
return {
2023-01-25 20:01:24 +01:00
globalId: this.defaultUserValues.globalUserId,
email: this.defaultUserValues.email,
firstName: this.defaultUserValues.firstName,
lastName: this.defaultUserValues.lastName,
}
}
async doInContext(appId: string | null, task: any) {
if (!appId) {
appId = this.appId
}
2023-01-25 18:11:37 +01:00
const tenant = this.getTenantId()
return tenancy.doInTenant(tenant, () => {
// check if already in a context
if (context.getAppId() == null && appId !== null) {
return context.doInAppContext(appId, async () => {
return task()
})
} else {
return task()
}
})
}
// SETUP / TEARDOWN
// use a new id as the name to avoid name collisions
async init(appName = newid()) {
if (!this.started) {
await startup()
}
return this.newTenant(appName)
}
2023-02-07 13:45:41 +01:00
end() {
if (!this) {
return
}
2023-01-17 11:28:51 +01:00
if (this.server) {
this.server.close()
} else {
require("../../app").default.close()
}
2023-02-07 13:45:41 +01:00
if (this.allApps) {
cleanup(this.allApps.map(app => app.appId))
}
}
// MODES
setMultiTenancy = (value: boolean) => {
env._set("MULTI_TENANCY", value)
coreEnv._set("MULTI_TENANCY", value)
}
setSelfHosted = (value: boolean) => {
env._set("SELF_HOSTED", value)
coreEnv._set("SELF_HOSTED", value)
}
2023-03-06 11:33:49 +01:00
setGoogleAuth = (value: string) => {
env._set("GOOGLE_CLIENT_ID", value)
env._set("GOOGLE_CLIENT_SECRET", value)
coreEnv._set("GOOGLE_CLIENT_ID", value)
coreEnv._set("GOOGLE_CLIENT_SECRET", value)
}
modeCloud = () => {
this.setSelfHosted(false)
}
modeSelf = () => {
this.setSelfHosted(true)
}
// UTILS
2023-01-17 11:28:51 +01:00
_req(body: any, params: any, controlFunc: any) {
2022-07-13 14:22:21 +02:00
// create a fake request ctx
const request: any = {}
2022-08-10 12:01:54 +02:00
const appId = this.appId
2022-07-13 14:22:21 +02:00
request.appId = appId
// fake cookies, we don't need them
request.cookies = { set: () => {}, get: () => {} }
2023-01-25 18:11:37 +01:00
request.user = { appId, tenantId: this.getTenantId() }
request.query = {}
request.request = {
2022-07-13 14:22:21 +02:00
body,
}
2022-07-13 14:22:21 +02:00
if (params) {
request.params = params
}
return this.doInContext(appId, async () => {
await controlFunc(request)
return request.body
})
}
// USER / AUTH
async globalUser({
2023-01-25 20:01:24 +01:00
id = this.defaultUserValues.globalUserId,
firstName = this.defaultUserValues.firstName,
lastName = this.defaultUserValues.lastName,
builder = true,
2022-05-20 22:16:29 +02:00
admin = false,
2023-01-25 20:01:24 +01:00
email = this.defaultUserValues.email,
roles,
}: any = {}) {
const db = tenancy.getTenantDB(this.getTenantId())
let existing
try {
existing = await db.get(id)
} catch (err) {
existing = { email }
}
const user = {
_id: id,
...existing,
roles: roles || {},
tenantId: this.getTenantId(),
firstName,
lastName,
}
await sessions.createASession(id, {
sessionId: "sessionid",
tenantId: this.getTenantId(),
csrfToken: this.defaultUserValues.csrfToken,
2022-01-25 23:54:50 +01:00
})
if (builder) {
user.builder = { global: true }
} else {
user.builder = { global: false }
}
if (admin) {
user.admin = { global: true }
} else {
user.admin = { global: false }
}
const resp = await db.put(user)
return {
_rev: resp.rev,
...user,
}
}
async createUser(
user: {
id?: string
firstName?: string
lastName?: string
email?: string
builder?: boolean
admin?: boolean
2023-03-29 17:42:55 +02:00
roles?: UserRoles
} = {}
) {
let { id, firstName, lastName, email, builder, admin, roles } = user
firstName = firstName || this.defaultUserValues.firstName
lastName = lastName || this.defaultUserValues.lastName
email = email || this.defaultUserValues.email
roles = roles || {}
if (builder == null) {
builder = true
}
const globalId = !id ? `us_${Math.random()}` : `us_${id}`
2022-05-20 22:16:29 +02:00
const resp = await this.globalUser({
id: globalId,
firstName,
lastName,
2022-05-20 22:16:29 +02:00
email,
builder,
admin,
roles,
})
await cache.user.invalidateUser(globalId)
return {
...resp,
globalId,
}
}
async login({ roleId, userId, builder, prodApp = false }: any = {}) {
const appId = prodApp ? this.prodAppId : this.appId
return context.doInAppContext(appId, async () => {
userId = !userId ? `us_uuid1` : userId
if (!this.request) {
throw "Server has not been opened, cannot login."
}
// make sure the user exists in the global DB
if (roleId !== roles.BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser({
id: userId,
builder,
roles: { [this.prodAppId]: roleId },
})
}
await sessions.createASession(userId, {
sessionId: "sessionid",
2023-01-25 18:11:37 +01:00
tenantId: this.getTenantId(),
})
// have to fake this
const authObj = {
userId,
sessionId: "sessionid",
2023-01-25 18:11:37 +01:00
tenantId: this.getTenantId(),
}
const authToken = auth.jwt.sign(authObj, coreEnv.JWT_SECRET)
// returning necessary request headers
await cache.user.invalidateUser(userId)
return {
Accept: "application/json",
Cookie: [`${constants.Cookie.Auth}=${authToken}`],
[constants.Header.APP_ID]: appId,
}
})
}
// HEADERS
2023-02-07 13:45:41 +01:00
defaultHeaders(extras = {}) {
2023-01-25 18:11:37 +01:00
const tenantId = this.getTenantId()
const authObj: AuthToken = {
2023-01-25 20:01:24 +01:00
userId: this.defaultUserValues.globalUserId,
2021-07-08 01:30:55 +02:00
sessionId: "sessionid",
2023-01-25 18:11:37 +01:00
tenantId,
}
const authToken = auth.jwt.sign(authObj, coreEnv.JWT_SECRET)
const headers: any = {
Accept: "application/json",
Cookie: [`${constants.Cookie.Auth}=${authToken}`],
2023-01-25 20:01:24 +01:00
[constants.Header.CSRF_TOKEN]: this.defaultUserValues.csrfToken,
Host: this.tenantHost(),
...extras,
}
2023-01-19 18:23:48 +01:00
if (this.appId) {
headers[constants.Header.APP_ID] = this.appId
}
return headers
}
publicHeaders({ prodApp = true } = {}) {
const appId = prodApp ? this.prodAppId : this.appId
const headers: any = {
Accept: "application/json",
}
2021-10-26 17:21:26 +02:00
if (appId) {
headers[constants.Header.APP_ID] = appId
}
2023-01-26 16:16:42 +01:00
headers[constants.Header.TENANT_ID] = this.getTenantId()
2023-01-26 16:16:42 +01:00
return headers
}
async roleHeaders({
2023-01-25 20:01:24 +01:00
email = this.defaultUserValues.email,
roleId = roles.BUILTIN_ROLE_IDS.ADMIN,
builder = false,
prodApp = true,
} = {}) {
return this.login({ email, roleId, builder, prodApp })
2021-03-08 15:49:19 +01:00
}
// TENANCY
tenantHost() {
const tenantId = this.getTenantId()
const platformHost = new URL(coreEnv.PLATFORM_URL).host.split(":")[0]
return `${tenantId}.${platformHost}`
}
getTenantId() {
if (!this.tenantId) {
throw new Error("no test tenant id - init has not been called")
}
return this.tenantId
}
async newTenant(appName = newid()): Promise<App> {
this.defaultUserValues = this.populateDefaultUserValues()
this.tenantId = structures.tenant.id()
this.user = await this.globalUser()
this.globalUserId = this.user._id
this.userMetadataId = generateUserMetadataID(this.globalUserId)
return this.createApp(appName)
}
doInTenant(task: any) {
return context.doInTenant(this.getTenantId(), task)
}
// API
2023-01-25 20:01:24 +01:00
async generateApiKey(userId = this.defaultUserValues.globalUserId) {
const db = tenancy.getTenantDB(this.getTenantId())
const id = dbCore.generateDevInfoID(userId)
let devInfo
try {
devInfo = await db.get(id)
} catch (err) {
devInfo = { _id: id, userId }
}
devInfo.apiKey = encryption.encrypt(
`${this.getTenantId()}${dbCore.SEPARATOR}${newid()}`
)
await db.put(devInfo)
return devInfo.apiKey
}
// APP
async createApp(appName: string): Promise<App> {
// create dev app
// clear any old app
this.appId = null
await context.doInAppContext(null, async () => {
this.app = await this._req(
{ name: appName },
null,
controllers.app.create
)
this.appId = this.app?.appId!
})
return await context.doInAppContext(this.appId, async () => {
// create production app
this.prodApp = await this.publish()
this.allApps.push(this.prodApp)
this.allApps.push(this.app)
return this.app
})
}
fixes for google sheets, admin checklist, and deleting an app from API (#8846) * fixes for google sheets, admin checklist, and deleting an app from API * code review * splitting unpublish endpoint, moving deploy endpoint to applications controller. Still to do public API work and move deployment controller into application controller * updating REST method for unpublish in API test * unpublish and publish endpoint on public API, delete endpoint unpublishes and deletes app * removing skip_setup from prodAppDb call * removing commented code * unit tests and open API spec updates * unpublish, publish unit tests - delete still in progress * remove line updating app name in API test * unit tests * v2.1.46 * Update pro version to 2.1.46 * v2.2.0 * Update pro version to 2.2.0 * Fix for budibase plugin skeleton, which utilises the old import style. * Fix side nav styles * v2.2.1 * Update pro version to 2.2.1 * using dist folder to allow importing constants for openAPI specs * v2.2.2 * Update pro version to 2.2.2 * Fix for user enrichment call (updating to @budibase/nano fork) (#9038) * Fix for #9029 - this should fix the issue users have been experiencing with user enrichment calls in apps, essentially it utilises a fork of the nano library we use to interact with CouchDB, which has been updated to use a POST request rather than a GET request as it supports a larger set of data being sent as query parameters. * Incrementing Nano version to attempt to fix yarn registry issues. * v2.2.3 * Update pro version to 2.2.3 * Fix SQL table `_id` filtering (#9030) * Re-add support for filtering on _id using external SQL tables and fix filter key prefixes not working with _id field * Remove like operator from internal tables and only allow basic operators on SQL table _id column * Update data section filtering to respect new rules * Update automation section filtering to respect new rules * Update dynamic filter component to respect new rules * v2.2.4 * Update pro version to 2.2.4 * lock changes (#9047) * v2.2.5 * Update pro version to 2.2.5 * Make looping arrow point in right direction (#9053) * v2.2.6 * Update pro version to 2.2.6 * Types/attaching license to account (#9065) * adding license type to account * removing planDuration * v2.2.7 * Update pro version to 2.2.7 * Environment variable type coercion fix (#9074) * Environment variable type coercion fix * Update .gitignore * v2.2.8 * Update pro version to 2.2.8 * tests passing * all tests passing, updates to public API response * update unpublish call to return 204, openAPI spec and unit * fixing API tests Co-authored-by: Budibase Release Bot <> Co-authored-by: mike12345567 <me@michaeldrury.co.uk> Co-authored-by: Andrew Kingston <andrew@kingston.dev> Co-authored-by: melohagan <101575380+melohagan@users.noreply.github.com> Co-authored-by: Rory Powell <rory.codes@gmail.com>
2022-12-19 14:18:00 +01:00
async publish() {
await this._req(null, null, controllers.deploy.publishApp)
// @ts-ignore
const prodAppId = this.getAppId().replace("_dev", "")
2022-07-13 14:22:21 +02:00
this.prodAppId = prodAppId
2022-08-10 12:01:54 +02:00
return context.doInAppContext(prodAppId, async () => {
2022-08-10 12:01:54 +02:00
const db = context.getProdAppDB()
2022-11-28 14:24:39 +01:00
return await db.get(dbCore.DocumentType.APP_METADATA)
})
}
fixes for google sheets, admin checklist, and deleting an app from API (#8846) * fixes for google sheets, admin checklist, and deleting an app from API * code review * splitting unpublish endpoint, moving deploy endpoint to applications controller. Still to do public API work and move deployment controller into application controller * updating REST method for unpublish in API test * unpublish and publish endpoint on public API, delete endpoint unpublishes and deletes app * removing skip_setup from prodAppDb call * removing commented code * unit tests and open API spec updates * unpublish, publish unit tests - delete still in progress * remove line updating app name in API test * unit tests * v2.1.46 * Update pro version to 2.1.46 * v2.2.0 * Update pro version to 2.2.0 * Fix for budibase plugin skeleton, which utilises the old import style. * Fix side nav styles * v2.2.1 * Update pro version to 2.2.1 * using dist folder to allow importing constants for openAPI specs * v2.2.2 * Update pro version to 2.2.2 * Fix for user enrichment call (updating to @budibase/nano fork) (#9038) * Fix for #9029 - this should fix the issue users have been experiencing with user enrichment calls in apps, essentially it utilises a fork of the nano library we use to interact with CouchDB, which has been updated to use a POST request rather than a GET request as it supports a larger set of data being sent as query parameters. * Incrementing Nano version to attempt to fix yarn registry issues. * v2.2.3 * Update pro version to 2.2.3 * Fix SQL table `_id` filtering (#9030) * Re-add support for filtering on _id using external SQL tables and fix filter key prefixes not working with _id field * Remove like operator from internal tables and only allow basic operators on SQL table _id column * Update data section filtering to respect new rules * Update automation section filtering to respect new rules * Update dynamic filter component to respect new rules * v2.2.4 * Update pro version to 2.2.4 * lock changes (#9047) * v2.2.5 * Update pro version to 2.2.5 * Make looping arrow point in right direction (#9053) * v2.2.6 * Update pro version to 2.2.6 * Types/attaching license to account (#9065) * adding license type to account * removing planDuration * v2.2.7 * Update pro version to 2.2.7 * Environment variable type coercion fix (#9074) * Environment variable type coercion fix * Update .gitignore * v2.2.8 * Update pro version to 2.2.8 * tests passing * all tests passing, updates to public API response * update unpublish call to return 204, openAPI spec and unit * fixing API tests Co-authored-by: Budibase Release Bot <> Co-authored-by: mike12345567 <me@michaeldrury.co.uk> Co-authored-by: Andrew Kingston <andrew@kingston.dev> Co-authored-by: melohagan <101575380+melohagan@users.noreply.github.com> Co-authored-by: Rory Powell <rory.codes@gmail.com>
2022-12-19 14:18:00 +01:00
async unpublish() {
const response = await this._req(
null,
{ appId: this.appId },
controllers.app.unpublish
)
this.prodAppId = null
this.prodApp = null
return response
}
// TABLE
2023-01-17 18:39:59 +01:00
async updateTable(config?: any): Promise<Table> {
config = config || basicTable()
2021-03-04 11:05:50 +01:00
this.table = await this._req(config, null, controllers.table.save)
return this.table
}
2023-01-17 18:22:31 +01:00
async createTable(config?: Table) {
if (config != null && config._id) {
delete config._id
}
return this.updateTable(config)
}
async getTable(tableId?: string) {
2021-03-04 15:36:59 +01:00
tableId = tableId || this.table._id
2022-02-28 19:53:03 +01:00
return this._req(null, { tableId }, controllers.table.find)
2021-03-04 15:36:59 +01:00
}
async createLinkedTable(relationshipType?: string, links: any = ["link"]) {
2021-03-04 14:07:33 +01:00
if (!this.table) {
throw "Must have created a table first."
}
const tableConfig: any = basicTable()
2021-03-04 14:07:33 +01:00
tableConfig.primaryDisplay = "name"
for (let link of links) {
tableConfig.schema[link] = {
type: "link",
fieldName: link,
tableId: this.table._id,
name: link,
}
if (relationshipType) {
tableConfig.schema[link].relationshipType = relationshipType
}
}
2021-03-04 14:07:33 +01:00
const linkedTable = await this.createTable(tableConfig)
this.linkedTable = linkedTable
return linkedTable
}
async createAttachmentTable() {
const table: any = basicTable()
table.schema.attachment = {
type: "attachment",
}
return this.createTable(table)
}
// ROW
2023-01-19 12:00:51 +01:00
async createRow(config?: Row): Promise<Row> {
if (!this.table) {
throw "Test requires table to be configured."
}
const tableId = (config && config.tableId) || this.table._id
config = config || basicRow(tableId)
return this._req(config, { tableId }, controllers.row.save)
}
2023-01-18 18:11:52 +01:00
async getRow(tableId: string, rowId: string): Promise<Row> {
return this._req(null, { tableId, rowId }, controllers.row.find)
}
async getRows(tableId: string) {
if (!tableId && this.table) {
tableId = this.table._id
}
2021-06-17 17:35:58 +02:00
return this._req(null, { tableId }, controllers.row.fetch)
}
2023-03-21 18:27:31 +01:00
async searchRows(tableId: string, searchParams: SearchFilters = {}) {
if (!tableId && this.table) {
tableId = this.table._id
}
const body = {
query: searchParams,
}
return this._req(body, { tableId }, controllers.row.search)
}
// ROLE
async createRole(config?: any) {
config = config || basicRole()
2021-03-04 11:05:50 +01:00
return this._req(config, null, controllers.role.save)
}
async addPermission(roleId: string, resourceId: string, level = "read") {
return this._req(
null,
{
roleId,
resourceId,
level,
},
2021-03-04 11:05:50 +01:00
controllers.perms.addPermission
)
}
// VIEW
async createView(config?: any) {
if (!this.table) {
throw "Test requires table to be configured."
}
const view = config || {
tableId: this.table._id,
name: "ViewTest",
}
2021-03-04 11:05:50 +01:00
return this._req(view, null, controllers.view.save)
}
// AUTOMATION
async createAutomation(config?: any) {
2021-03-03 19:41:49 +01:00
config = config || basicAutomation()
if (config._rev) {
delete config._rev
}
this.automation = (
2021-03-04 11:05:50 +01:00
await this._req(config, null, controllers.automation.create)
2021-03-03 19:41:49 +01:00
).automation
return this.automation
}
async getAllAutomations() {
2021-03-04 11:05:50 +01:00
return this._req(null, null, controllers.automation.fetch)
2021-03-03 19:41:49 +01:00
}
async deleteAutomation(automation?: any) {
2021-03-03 19:41:49 +01:00
automation = automation || this.automation
if (!automation) {
return
}
return this._req(
null,
{ id: automation._id, rev: automation._rev },
2021-03-04 11:05:50 +01:00
controllers.automation.destroy
2021-03-03 19:41:49 +01:00
)
}
async createWebhook(config?: any) {
if (!this.automation) {
throw "Must create an automation before creating webhook."
}
config = config || basicWebhook(this.automation._id)
return (await this._req(config, null, controllers.webhook.save)).webhook
}
// DATASOURCE
2023-01-20 11:29:11 +01:00
async createDatasource(config?: {
datasource: Datasource
}): Promise<Datasource> {
2021-03-04 11:05:50 +01:00
config = config || basicDatasource()
2023-01-20 11:29:11 +01:00
const response = await this._req(config, null, controllers.datasource.save)
2021-10-27 14:10:46 +02:00
this.datasource = response.datasource
2021-03-04 11:05:50 +01:00
return this.datasource
}
async updateDatasource(datasource: any) {
const response = await this._req(
datasource,
{ datasourceId: datasource._id },
controllers.datasource.update
)
this.datasource = response.datasource
return this.datasource
}
async restDatasource(cfg?: any) {
return this.createDatasource({
2023-01-20 11:29:11 +01:00
datasource: {
...basicDatasource().datasource,
source: SourceName.REST,
config: cfg || {},
},
})
}
async dynamicVariableDatasource() {
let datasource = await this.restDatasource()
const basedOnQuery = await this.createQuery({
2023-01-18 11:45:42 +01:00
...basicQuery(datasource._id!),
fields: {
path: "www.google.com",
},
})
datasource = await this.updateDatasource({
...datasource,
config: {
dynamicVariables: [
{
queryId: basedOnQuery._id,
name: "variable3",
value: "{{ data.0.[value] }}",
},
],
},
})
return { datasource, query: basedOnQuery }
}
// QUERY
async previewQuery(
request: any,
config: any,
datasource: any,
fields: any,
params: any,
2023-01-20 13:12:59 +01:00
verb?: string
) {
return request
.post(`/api/queries/preview`)
.send({
datasourceId: datasource._id,
parameters: params || {},
fields,
queryVerb: verb || "read",
2022-02-11 17:28:19 +01:00
name: datasource.name,
})
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
}
async createQuery(config?: any) {
2021-03-04 11:05:50 +01:00
if (!this.datasource && !config) {
throw "No datasource created for query."
2021-03-04 11:05:50 +01:00
}
config = config || basicQuery(this.datasource._id)
return this._req(config, null, controllers.query.save)
}
// SCREEN
async createScreen(config?: any) {
2021-03-08 15:49:19 +01:00
config = config || basicScreen()
return this._req(config, null, controllers.screen.save)
}
// LAYOUT
async createLayout(config?: any) {
config = config || basicLayout()
return await this._req(config, null, controllers.layout.save)
}
}
export = TestConfiguration