Merge pull request #9963 from Budibase/api-tets-public-api-key-generation

QA-6 Api tests public api key generation
This commit is contained in:
Pedro Silva 2023-03-14 15:40:36 +00:00 committed by GitHub
commit df4f6c3b09
18 changed files with 341 additions and 43 deletions

View File

@ -1,6 +1,5 @@
BB_ADMIN_USER_EMAIL=qa@budibase.com BB_ADMIN_USER_EMAIL=qa@budibase.com
BB_ADMIN_USER_PASSWORD=budibase BB_ADMIN_USER_PASSWORD=budibase
ENCRYPTED_TEST_PUBLIC_API_KEY=a65722f06bee5caeadc5d7ca2f543a43-d610e627344210c643bb726f
COUCH_DB_URL=http://budibase:budibase@localhost:4567 COUCH_DB_URL=http://budibase:budibase@localhost:4567
COUCH_DB_USER=budibase COUCH_DB_USER=budibase
COUCH_DB_PASSWORD=budibase COUCH_DB_PASSWORD=budibase

View File

@ -1,11 +1,3 @@
const env = require("../src/environment")
env._set("BUDIBASE_SERVER_URL", "http://localhost:4100")
env._set(
"BUDIBASE_PUBLIC_API_KEY",
"a65722f06bee5caeadc5d7ca2f543a43-d610e627344210c643bb726f"
)
// mock all dates to 2020-01-01T00:00:00.000Z // mock all dates to 2020-01-01T00:00:00.000Z
// use tk.reset() to use real dates in individual tests // use tk.reset() to use real dates in individual tests
const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z") const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z")

View File

@ -49,14 +49,16 @@ class InternalAPIClient {
// @ts-ignore // @ts-ignore
const response = await fetch(`https://${process.env.TENANT_ID}.${this.host}${url}`, requestOptions) const response = await fetch(`https://${process.env.TENANT_ID}.${this.host}${url}`, requestOptions)
if (response.status == 404 || response.status == 500) { if (
response.status == 404 ||
response.status == 500 ||
response.status == 403
) {
console.error("Error in apiCall") console.error("Error in apiCall")
console.error("Response:") console.error("Response:", response)
console.error(response) const json = await response.json()
console.error("Response body:") console.error("Response body:", json)
console.error(response.body) console.error("Request body:", requestOptions.body)
console.error("Request body:")
console.error(requestOptions.body)
} }
return response return response
} }

View File

@ -44,12 +44,10 @@ class AccountsAPIClient {
const response = await fetch(`${this.host}${url}`, requestOptions) const response = await fetch(`${this.host}${url}`, requestOptions)
if (response.status == 404 || response.status == 500) { if (response.status == 404 || response.status == 500) {
console.error("Error in apiCall") console.error("Error in apiCall")
console.error("Response:") console.error("Response:", response)
console.error(response) const json = await response.json()
console.error("Response body:") console.error("Response body:", json)
console.error(response.body) console.error("Request body:", requestOptions.body)
console.error("Request body:")
console.error(requestOptions.body)
} }
return response return response
} }

View File

@ -5,7 +5,8 @@ import { Hosting } from "@budibase/types"
export const generateAccount = (): Partial<NewAccount> => { export const generateAccount = (): Partial<NewAccount> => {
const randomGuid = generator.guid() const randomGuid = generator.guid()
let tenant: string = "a" + randomGuid //Needs to start with a letter
let tenant: string = "tenant" + randomGuid
tenant = tenant.replace(/-/g, "") tenant = tenant.replace(/-/g, "")
return { return {

View File

@ -11,20 +11,32 @@ interface ApiOptions {
class PublicAPIClient { class PublicAPIClient {
host: string host: string
apiKey: string apiKey?: string
tenantName?: string
appId?: string appId?: string
cookie?: string
constructor(appId?: string) { constructor(appId?: string) {
if (!env.BUDIBASE_PUBLIC_API_KEY || !env.BUDIBASE_SERVER_URL) { if (!env.BUDIBASE_HOST) {
throw new Error( throw new Error(
"Must set BUDIBASE_PUBLIC_API_KEY and BUDIBASE_SERVER_URL env vars" "Must set BUDIBASE_PUBLIC_API_KEY and BUDIBASE_SERVER_URL env vars"
) )
} }
this.host = `${env.BUDIBASE_SERVER_URL}/api/public/v1` this.host = `${env.BUDIBASE_HOST}/api/public/v1`
this.apiKey = env.BUDIBASE_PUBLIC_API_KEY
this.appId = appId this.appId = appId
} }
setTenantName(tenantName: string) {
this.tenantName = tenantName
}
setApiKey(apiKey: string) {
this.apiKey = apiKey
process.env.BUDIBASE_PUBLIC_API_KEY = apiKey
this.host = `${env.BUDIBASE_HOST}/api/public/v1`
}
apiCall = apiCall =
(method: APIMethod) => (method: APIMethod) =>
async (url = "", options: ApiOptions = {}) => { async (url = "", options: ApiOptions = {}) => {
@ -32,18 +44,27 @@ class PublicAPIClient {
method, method,
body: JSON.stringify(options.body), body: JSON.stringify(options.body),
headers: { headers: {
"x-budibase-api-key": this.apiKey, "x-budibase-api-key": this.apiKey || null,
"x-budibase-app-id": this.appId, "x-budibase-app-id": this.appId,
"Content-Type": "application/json", "Content-Type": "application/json",
Accept: "application/json", Accept: "application/json",
...options.headers, ...options.headers,
cookie: this.cookie,
redirect: "follow",
follow: 20,
}, },
} }
// prettier-ignore
// @ts-ignore // @ts-ignore
const response = await fetch(`${this.host}${url}`, requestOptions) const response = await fetch(`https://${process.env.TENANT_ID}.${this.host}${url}`, requestOptions)
if (response.status !== 200) {
console.error(response) if (response.status == 500 || response.status == 403) {
console.error("Error in apiCall")
console.error("Response:", response)
const json = await response.json()
console.error("Response body:", json)
console.error("Request body:", requestOptions.body)
} }
return response return response
} }

View File

@ -0,0 +1,38 @@
import { Response } from "node-fetch"
import { Account } from "@budibase/types"
import AccountsAPIClient from "./accountsAPIClient"
import { NewAccount } from "../fixtures/types/newAccount"
export default class AccountsApi {
api: AccountsAPIClient
constructor(AccountsAPIClient: AccountsAPIClient) {
this.api = AccountsAPIClient
}
async validateEmail(email: string): Promise<Response> {
const response = await this.api.post(`/accounts/validate/email`, {
body: { email },
})
expect(response).toHaveStatusCode(200)
return response
}
async validateTenantId(tenantId: string): Promise<Response> {
const response = await this.api.post(`/accounts/validate/tenantId`, {
body: { tenantId },
})
expect(response).toHaveStatusCode(200)
return response
}
async create(body: Partial<NewAccount>): Promise<[Response, Account]> {
const headers = {
"no-verify": "1",
}
const response = await this.api.post(`/accounts`, { body, headers })
const json = await response.json()
expect(response).toHaveStatusCode(201)
return [response, json]
}
}

View File

@ -0,0 +1,66 @@
import env from "../../../environment"
import fetch, { HeadersInit } from "node-fetch"
type APIMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
interface ApiOptions {
method?: APIMethod
body?: object
headers?: HeadersInit | undefined
}
class AccountsAPIClient {
host: string
appId?: string
cookie?: string
constructor(appId?: string) {
if (!env.BUDIBASE_ACCOUNTS_URL) {
throw new Error("Must set BUDIBASE_SERVER_URL env var")
}
this.host = `${env.BUDIBASE_ACCOUNTS_URL}/api`
this.appId = appId
}
apiCall =
(method: APIMethod) =>
async (url = "", options: ApiOptions = {}) => {
const requestOptions = {
method,
body: JSON.stringify(options.body),
headers: {
"x-budibase-app-id": this.appId,
"Content-Type": "application/json",
Accept: "application/json",
cookie: this.cookie,
redirect: "follow",
follow: 20,
...options.headers,
},
credentials: "include",
}
// @ts-ignore
const response = await fetch(`${this.host}${url}`, requestOptions)
if (
response.status == 404 ||
response.status == 500 ||
response.status == 400
) {
console.error("Error in apiCall")
console.error("Response:", response)
const json = await response.json()
console.error("Response body:", json)
console.error("Request body:", requestOptions.body)
}
return response
}
post = this.apiCall("POST")
get = this.apiCall("GET")
patch = this.apiCall("PATCH")
del = this.apiCall("DELETE")
put = this.apiCall("PUT")
}
export default AccountsAPIClient

View File

@ -63,4 +63,15 @@ export default class AppApi {
const response = await this.api.post(`/applications/${id}/unpublish`) const response = await this.api.post(`/applications/${id}/unpublish`)
return [response] return [response]
} }
async createFirstApp() {
const body = {
name: "My first app",
url: "my-first-app",
useTemplate: false,
sampleData: true,
}
const response = await this.api.post("/applications", { body })
expect(response).toHaveStatusCode(200)
}
} }

View File

@ -0,0 +1,48 @@
import { Response } from "node-fetch"
import AccountsAPIClient from "./accountsAPIClient"
import { ApiKeyResponse } from "../fixtures/types/apiKeyResponse"
export default class AuthApi {
api: AccountsAPIClient
constructor(apiClient: AccountsAPIClient) {
this.api = apiClient
}
async loginAsAdmin(): Promise<[Response, any]> {
const response = await this.api.post(`/auth/login`, {
body: {
username: process.env.BB_ADMIN_USER_EMAIL,
password: process.env.BB_ADMIN_USER_PASSWORD,
},
})
const cookie = response.headers.get("set-cookie")
this.api.cookie = cookie as any
return [response, cookie]
}
async login(email: String, password: String): Promise<[Response, any]> {
const response = await this.api.post(`/global/auth/default/login`, {
body: {
username: email,
password: password,
},
})
expect(response).toHaveStatusCode(200)
const cookie = response.headers.get("set-cookie")
this.api.cookie = cookie as any
return [response, cookie]
}
async logout(): Promise<any> {
return this.api.post(`/global/auth/logout`)
}
async getApiKey(): Promise<ApiKeyResponse> {
const response = await this.api.get(`/global/self/api_key`)
const json = await response.json()
expect(response).toHaveStatusCode(200)
expect(json).toHaveProperty("apiKey")
return json
}
}

View File

@ -3,19 +3,82 @@ import ApplicationApi from "./applications"
import TableApi from "./tables" import TableApi from "./tables"
import UserApi from "./users" import UserApi from "./users"
import RowApi from "./rows" import RowApi from "./rows"
import AuthApi from "./auth"
import AccountsApiClient from "./accountsAPIClient"
import AccountsApi from "./accounts"
import { generateAccount } from "../fixtures/accounts"
import internalApplicationsApi from "../../internal-api/TestConfiguration/applications"
import InternalAPIClient from "../../internal-api/TestConfiguration/InternalAPIClient"
export default class TestConfiguration<T> { export default class TestConfiguration<T> {
applications: ApplicationApi applications: ApplicationApi
auth: AuthApi
users: UserApi users: UserApi
tables: TableApi tables: TableApi
rows: RowApi rows: RowApi
context: T context: T
accounts: AccountsApi
apiClient: PublicAPIClient
accountsApiClient: AccountsApiClient
internalApiClient: InternalAPIClient
internalApplicationsApi: internalApplicationsApi
constructor(apiClient: PublicAPIClient) { constructor(
apiClient: PublicAPIClient,
accountsApiClient: AccountsApiClient,
internalApiClient: InternalAPIClient
) {
this.apiClient = apiClient
this.accountsApiClient = accountsApiClient
this.internalApiClient = internalApiClient
this.auth = new AuthApi(this.internalApiClient)
this.accounts = new AccountsApi(this.accountsApiClient)
this.applications = new ApplicationApi(apiClient) this.applications = new ApplicationApi(apiClient)
this.users = new UserApi(apiClient) this.users = new UserApi(apiClient)
this.tables = new TableApi(apiClient) this.tables = new TableApi(apiClient)
this.rows = new RowApi(apiClient) this.rows = new RowApi(apiClient)
this.internalApplicationsApi = new internalApplicationsApi(
internalApiClient
)
this.context = <T>{}
}
async setupAccountAndTenant() {
// This step is required to create a new account and tenant for the tests, its part of
// the support for running tests in multiple environments.
const account = generateAccount()
await this.accounts.validateEmail(<string>account.email)
await this.accounts.validateTenantId(<string>account.tenantId)
process.env.TENANT_ID = <string>account.tenantId
await this.accounts.create(account)
await this.updateApiClients(<string>account.tenantName)
await this.auth.login(<string>account.email, <string>account.password)
const body = {
name: "My first app",
url: "my-first-app",
useTemplate: false,
sampleData: true,
}
await this.internalApplicationsApi.create(body)
}
// After the account and tenant have been created, we need to get and set the API key for the test
async setApiKey() {
const apiKeyResponse = await this.auth.getApiKey()
this.apiClient.setApiKey(apiKeyResponse.apiKey)
}
async updateApiClients(tenantName: string) {
this.apiClient.setTenantName(tenantName)
this.applications = new ApplicationApi(this.apiClient)
this.rows = new RowApi(this.apiClient)
this.internalApiClient.setTenantName(tenantName)
this.internalApplicationsApi = new internalApplicationsApi(
this.internalApiClient
)
this.auth = new AuthApi(this.internalApiClient)
this.context = <T>{} this.context = <T>{}
} }

View File

@ -0,0 +1,22 @@
import { NewAccount } from "./types/newAccount"
import generator from "../../generator"
import { Hosting } from "@budibase/types"
export const generateAccount = (): Partial<NewAccount> => {
const randomGuid = generator.guid()
//Needs to start with a letter
let tenant: string = "tenant" + randomGuid
tenant = tenant.replace(/-/g, "")
return {
email: `qa+${randomGuid}@budibase.com`,
hosting: Hosting.CLOUD,
name: `qa+${randomGuid}@budibase.com`,
password: `${randomGuid}`,
profession: "software_engineer",
size: "10+",
tenantId: `${tenant}`,
tenantName: `${tenant}`,
}
}

View File

@ -0,0 +1,6 @@
export interface ApiKeyResponse {
apiKey: string
createdAt: string
updatedAt: string
userId: string
}

View File

@ -0,0 +1,5 @@
import { Account } from "@budibase/types"
export interface NewAccount extends Account {
password: string
}

View File

@ -1,15 +1,25 @@
import TestConfiguration from "../../../config/public-api/TestConfiguration" import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient" import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
import AccountsAPIClient from "../../../config/public-api/TestConfiguration/accountsAPIClient"
import generateApp from "../../../config/public-api/fixtures/applications" import generateApp from "../../../config/public-api/fixtures/applications"
import { Application } from "@budibase/server/api/controllers/public/mapping/types" import { Application } from "@budibase/server/api/controllers/public/mapping/types"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient"
describe("Public API - /applications endpoints", () => { describe("Public API - /applications endpoints", () => {
const api = new PublicAPIClient() const api = new PublicAPIClient()
const config = new TestConfiguration<Application>(api) const accountsAPI = new AccountsAPIClient()
const internalAPI = new InternalAPIClient()
const config = new TestConfiguration<Application>(
api,
accountsAPI,
internalAPI
)
beforeAll(async () => { beforeAll(async () => {
await config.beforeAll() await config.setupAccountAndTenant()
await config.setApiKey()
const [response, app] = await config.applications.seed() const [response, app] = await config.applications.seed()
config.context = app config.context = app
}) })

View File

@ -2,14 +2,19 @@ import { Row } from "@budibase/server/api/controllers/public/mapping/types"
import { generateRow } from "../../../config/public-api/fixtures/tables" import { generateRow } from "../../../config/public-api/fixtures/tables"
import TestConfiguration from "../../../config/public-api/TestConfiguration" import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient" import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
import AccountsAPIClient from "../../../config/public-api/TestConfiguration/accountsAPIClient"
import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient"
describe("Public API - /rows endpoints", () => { describe("Public API - /rows endpoints", () => {
let api = new PublicAPIClient() const api = new PublicAPIClient()
const accountsAPI = new AccountsAPIClient()
const config = new TestConfiguration<Row>(api) const internalAPI = new InternalAPIClient()
const config = new TestConfiguration<Row>(api, accountsAPI, internalAPI)
beforeAll(async () => { beforeAll(async () => {
await config.beforeAll() await config.setupAccountAndTenant()
await config.setApiKey()
const [aResp, app] = await config.applications.seed() const [aResp, app] = await config.applications.seed()
config.tables.api.appId = app._id config.tables.api.appId = app._id

View File

@ -2,13 +2,19 @@ import { Table } from "@budibase/server/api/controllers/public/mapping/types"
import { generateTable } from "../../../config/public-api/fixtures/tables" import { generateTable } from "../../../config/public-api/fixtures/tables"
import TestConfiguration from "../../../config/public-api/TestConfiguration" import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient" import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
import AccountsAPIClient from "../../../config/public-api/TestConfiguration/accountsAPIClient"
import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient"
describe("Public API - /tables endpoints", () => { describe("Public API - /tables endpoints", () => {
let api = new PublicAPIClient() const api = new PublicAPIClient()
const config = new TestConfiguration<Table>(api) const accountsAPI = new AccountsAPIClient()
const internalAPI = new InternalAPIClient()
const config = new TestConfiguration<Table>(api, accountsAPI, internalAPI)
beforeAll(async () => { beforeAll(async () => {
await config.beforeAll() await config.setupAccountAndTenant()
await config.setApiKey()
const [appResp, app] = await config.applications.seed() const [appResp, app] = await config.applications.seed()
config.tables.api.appId = app._id config.tables.api.appId = app._id

View File

@ -2,13 +2,18 @@ import TestConfiguration from "../../../config/public-api/TestConfiguration"
import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient" import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient"
import generateUser from "../../../config/public-api/fixtures/users" import generateUser from "../../../config/public-api/fixtures/users"
import { User } from "@budibase/server/api/controllers/public/mapping/types" import { User } from "@budibase/server/api/controllers/public/mapping/types"
import AccountsAPIClient from "../../../config/public-api/TestConfiguration/accountsAPIClient"
import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient"
describe("Public API - /users endpoints", () => { describe("Public API - /users endpoints", () => {
const api = new PublicAPIClient() const api = new PublicAPIClient()
const config = new TestConfiguration<User>(api) const accountsAPI = new AccountsAPIClient()
const internalAPI = new InternalAPIClient()
const config = new TestConfiguration<User>(api, accountsAPI, internalAPI)
beforeAll(async () => { beforeAll(async () => {
await config.beforeAll() await config.setupAccountAndTenant()
await config.setApiKey()
const [_, user] = await config.users.seed() const [_, user] = await config.users.seed()
config.context = user config.context = user
}) })