Merge pull request #1266 from Budibase/middleware-tests
Middleware tests
This commit is contained in:
commit
f69e06870b
|
@ -31,6 +31,7 @@ module.exports = async (ctx, next) => {
|
||||||
token = ctx.cookies.get(getCookieName())
|
token = ctx.cookies.get(getCookieName())
|
||||||
authType = AuthTypes.BUILDER
|
authType = AuthTypes.BUILDER
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!token && appId) {
|
if (!token && appId) {
|
||||||
token = ctx.cookies.get(getCookieName(appId))
|
token = ctx.cookies.get(getCookieName(appId))
|
||||||
authType = AuthTypes.APP
|
authType = AuthTypes.APP
|
||||||
|
@ -58,6 +59,7 @@ module.exports = async (ctx, next) => {
|
||||||
role: await getRole(appId, jwtPayload.roleId),
|
role: await getRole(appId, jwtPayload.roleId),
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
if (authType === AuthTypes.BUILDER) {
|
if (authType === AuthTypes.BUILDER) {
|
||||||
clearCookie(ctx)
|
clearCookie(ctx)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
|
|
|
@ -24,6 +24,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
||||||
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
|
if (!env.CLOUD && LOCAL_PASS.test(ctx.request.url)) {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.CLOUD && ctx.headers["x-api-key"] && ctx.headers["x-instanceid"]) {
|
if (env.CLOUD && ctx.headers["x-api-key"] && ctx.headers["x-instanceid"]) {
|
||||||
// api key header passed by external webhook
|
// api key header passed by external webhook
|
||||||
if (await isAPIKeyValid(ctx.headers["x-api-key"])) {
|
if (await isAPIKeyValid(ctx.headers["x-api-key"])) {
|
||||||
|
@ -37,14 +38,14 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.throw(403, "API key invalid")
|
return ctx.throw(403, "API key invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't expose builder endpoints in the cloud
|
// don't expose builder endpoints in the cloud
|
||||||
if (env.CLOUD && permType === PermissionTypes.BUILDER) return
|
if (env.CLOUD && permType === PermissionTypes.BUILDER) return
|
||||||
|
|
||||||
if (!ctx.user) {
|
if (!ctx.user) {
|
||||||
ctx.throw(403, "No user info found")
|
return ctx.throw(403, "No user info found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = ctx.user.role
|
const role = ctx.user.role
|
||||||
|
@ -52,7 +53,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
||||||
ctx.appId,
|
ctx.appId,
|
||||||
role._id
|
role._id
|
||||||
)
|
)
|
||||||
const isAdmin = ADMIN_ROLES.indexOf(role._id) !== -1
|
const isAdmin = ADMIN_ROLES.includes(role._id)
|
||||||
const isAuthed = ctx.auth.authenticated
|
const isAuthed = ctx.auth.authenticated
|
||||||
|
|
||||||
// this may need to change in the future, right now only admins
|
// this may need to change in the future, right now only admins
|
||||||
|
@ -61,7 +62,7 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => {
|
||||||
if (isAdmin && isAuthed) {
|
if (isAdmin && isAuthed) {
|
||||||
return next()
|
return next()
|
||||||
} else if (permType === PermissionTypes.BUILDER) {
|
} else if (permType === PermissionTypes.BUILDER) {
|
||||||
ctx.throw(403, "Not Authorized")
|
return ctx.throw(403, "Not Authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -36,6 +36,8 @@ class ResourceIdGetter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.ResourceIdGetter = ResourceIdGetter
|
||||||
|
|
||||||
module.exports.paramResource = main => {
|
module.exports.paramResource = main => {
|
||||||
return new ResourceIdGetter("params").mainResource(main).build()
|
return new ResourceIdGetter("params").mainResource(main).build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Authenticated middleware sets the correct APP auth type information when the user is not in the builder 1`] = `
|
||||||
|
Object {
|
||||||
|
"apiKey": "1234",
|
||||||
|
"appId": "budibase:app:local",
|
||||||
|
"role": Role {
|
||||||
|
"_id": "ADMIN",
|
||||||
|
"inherits": "POWER",
|
||||||
|
"name": "Admin",
|
||||||
|
"permissionId": "admin",
|
||||||
|
},
|
||||||
|
"roleId": "ADMIN",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Authenticated middleware sets the correct BUILDER auth type information when the x-budibase-type header is not 'client' 1`] = `
|
||||||
|
Object {
|
||||||
|
"apiKey": "1234",
|
||||||
|
"appId": "budibase:builder:local",
|
||||||
|
"role": Role {
|
||||||
|
"_id": "BUILDER",
|
||||||
|
"name": "Builder",
|
||||||
|
"permissionId": "admin",
|
||||||
|
},
|
||||||
|
"roleId": "BUILDER",
|
||||||
|
}
|
||||||
|
`;
|
|
@ -0,0 +1,126 @@
|
||||||
|
const { AuthTypes } = require("../../constants")
|
||||||
|
const authenticatedMiddleware = require("../authenticated")
|
||||||
|
const jwt = require("jsonwebtoken")
|
||||||
|
jest.mock("jsonwebtoken")
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
constructor(middleware) {
|
||||||
|
this.middleware = authenticatedMiddleware
|
||||||
|
this.ctx = {
|
||||||
|
config: {},
|
||||||
|
auth: {},
|
||||||
|
request: {},
|
||||||
|
cookies: {
|
||||||
|
set: jest.fn(),
|
||||||
|
get: jest.fn()
|
||||||
|
},
|
||||||
|
headers: {},
|
||||||
|
params: {},
|
||||||
|
path: "",
|
||||||
|
request: {
|
||||||
|
headers: {}
|
||||||
|
},
|
||||||
|
throw: jest.fn()
|
||||||
|
}
|
||||||
|
this.next = jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeaders(headers) {
|
||||||
|
this.ctx.headers = headers
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMiddleware() {
|
||||||
|
return this.middleware(this.ctx, this.next)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach() {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Authenticated middleware", () => {
|
||||||
|
let config
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.afterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls next() when on the builder path", async () => {
|
||||||
|
config.ctx.path = "/_builder"
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets a new cookie when the current cookie does not match the app id from context", async () => {
|
||||||
|
const appId = "app_123"
|
||||||
|
config.setHeaders({
|
||||||
|
"x-budibase-app-id": appId
|
||||||
|
})
|
||||||
|
config.ctx.cookies.get.mockImplementation(() => "cookieAppId")
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.cookies.set).toHaveBeenCalledWith(
|
||||||
|
"budibase:currentapp:local",
|
||||||
|
appId,
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets the correct BUILDER auth type information when the x-budibase-type header is not 'client'", async () => {
|
||||||
|
config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local")
|
||||||
|
jwt.verify.mockImplementationOnce(() => ({
|
||||||
|
apiKey: "1234",
|
||||||
|
roleId: "BUILDER"
|
||||||
|
}))
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.auth.authenticated).toEqual(AuthTypes.BUILDER)
|
||||||
|
expect(config.ctx.user).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets the correct APP auth type information when the user is not in the builder", async () => {
|
||||||
|
config.setHeaders({
|
||||||
|
"x-budibase-type": "client"
|
||||||
|
})
|
||||||
|
config.ctx.cookies.get.mockImplementation(() => `budibase:app:local`)
|
||||||
|
jwt.verify.mockImplementationOnce(() => ({
|
||||||
|
apiKey: "1234",
|
||||||
|
roleId: "ADMIN"
|
||||||
|
}))
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.auth.authenticated).toEqual(AuthTypes.APP)
|
||||||
|
expect(config.ctx.user).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("marks the user as unauthenticated when a token cannot be determined from the users cookie", async () => {
|
||||||
|
config.executeMiddleware()
|
||||||
|
expect(config.ctx.auth.authenticated).toBe(false)
|
||||||
|
expect(config.ctx.user.role).toEqual({
|
||||||
|
_id: "PUBLIC",
|
||||||
|
name: "Public",
|
||||||
|
permissionId: "public"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("clears the cookie when there is an error authenticating in the builder", async () => {
|
||||||
|
config.ctx.cookies.get.mockImplementation(() => "budibase:builder:local")
|
||||||
|
jwt.verify.mockImplementationOnce(() => {
|
||||||
|
throw new Error()
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.cookies.set).toBeCalledWith("budibase:builder:local")
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,196 @@
|
||||||
|
const authorizedMiddleware = require("../authorized")
|
||||||
|
const env = require("../../environment")
|
||||||
|
const apiKey = require("../../utilities/security/apikey")
|
||||||
|
const { AuthTypes } = require("../../constants")
|
||||||
|
const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions")
|
||||||
|
const { Test } = require("supertest")
|
||||||
|
jest.mock("../../environment")
|
||||||
|
jest.mock("../../utilities/security/apikey")
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
constructor(role) {
|
||||||
|
this.middleware = authorizedMiddleware(role)
|
||||||
|
this.next = jest.fn()
|
||||||
|
this.throw = jest.fn()
|
||||||
|
this.ctx = {
|
||||||
|
headers: {},
|
||||||
|
request: {
|
||||||
|
url: ""
|
||||||
|
},
|
||||||
|
auth: {},
|
||||||
|
next: this.next,
|
||||||
|
throw: this.throw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMiddleware() {
|
||||||
|
return this.middleware(this.ctx, this.next)
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(user) {
|
||||||
|
this.ctx.user = user
|
||||||
|
}
|
||||||
|
|
||||||
|
setMiddlewareRequiredPermission(...perms) {
|
||||||
|
this.middleware = authorizedMiddleware(...perms)
|
||||||
|
}
|
||||||
|
|
||||||
|
setResourceId(id) {
|
||||||
|
this.ctx.resourceId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthenticated(isAuthed) {
|
||||||
|
this.ctx.auth = { authenticated: isAuthed }
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestUrl(url) {
|
||||||
|
this.ctx.request.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
setCloudEnv(isCloud) {
|
||||||
|
env.CLOUD = isCloud
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestHeaders(headers) {
|
||||||
|
this.ctx.headers = headers
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach() {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe("Authorization middleware", () => {
|
||||||
|
const next = jest.fn()
|
||||||
|
let config
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.afterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("passes the middleware for local webhooks", async () => {
|
||||||
|
config.setRequestUrl("https://something/webhooks/trigger")
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("external web hook call", () => {
|
||||||
|
let ctx = {}
|
||||||
|
let middleware
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
config.setCloudEnv(true)
|
||||||
|
config.setRequestHeaders({
|
||||||
|
"x-api-key": "abc123",
|
||||||
|
"x-instanceid": "instance123",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("passes to next() if api key is valid", async () => {
|
||||||
|
apiKey.isAPIKeyValid.mockResolvedValueOnce(true)
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
expect(config.ctx.auth).toEqual({
|
||||||
|
authenticated: AuthTypes.EXTERNAL,
|
||||||
|
apiKey: config.ctx.headers["x-api-key"],
|
||||||
|
})
|
||||||
|
expect(config.ctx.user).toEqual({
|
||||||
|
appId: config.ctx.headers["x-instanceid"],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if api key is invalid", async () => {
|
||||||
|
apiKey.isAPIKeyValid.mockResolvedValueOnce(false)
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(403, "API key invalid")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("non-webhook call", () => {
|
||||||
|
let config
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
config.setCloudEnv(true)
|
||||||
|
config.setAuthenticated(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws when no user data is present in context", async () => {
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(403, "No user info found")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("passes on to next() middleware if user is an admin", async () => {
|
||||||
|
config.setUser({
|
||||||
|
role: {
|
||||||
|
_id: "ADMIN",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if the user has only builder permissions", async () => {
|
||||||
|
config.setCloudEnv(false)
|
||||||
|
config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER)
|
||||||
|
config.setUser({
|
||||||
|
role: {
|
||||||
|
_id: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(403, "Not Authorized")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("passes on to next() middleware if the user has resource permission", async () => {
|
||||||
|
config.setResourceId(PermissionTypes.QUERY)
|
||||||
|
config.setUser({
|
||||||
|
role: {
|
||||||
|
_id: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
config.setMiddlewareRequiredPermission(PermissionTypes.QUERY)
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if the user session is not authenticated after permission checks", async () => {
|
||||||
|
config.setUser({
|
||||||
|
role: {
|
||||||
|
_id: ""
|
||||||
|
},
|
||||||
|
})
|
||||||
|
config.setAuthenticated(false)
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(403, "Session not authenticated")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if the user does not have base permissions to perform the operation", async () => {
|
||||||
|
config.setUser({
|
||||||
|
role: {
|
||||||
|
_id: ""
|
||||||
|
},
|
||||||
|
})
|
||||||
|
config.setMiddlewareRequiredPermission(PermissionTypes.ADMIN, PermissionLevels.BASIC)
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,105 @@
|
||||||
|
const {
|
||||||
|
paramResource,
|
||||||
|
paramSubResource,
|
||||||
|
bodyResource,
|
||||||
|
bodySubResource,
|
||||||
|
ResourceIdGetter
|
||||||
|
} = require("../resourceId")
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
constructor(middleware) {
|
||||||
|
this.middleware = middleware
|
||||||
|
this.ctx = {
|
||||||
|
request: {},
|
||||||
|
}
|
||||||
|
this.next = jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
setParams(params) {
|
||||||
|
this.ctx.params = params
|
||||||
|
}
|
||||||
|
|
||||||
|
setBody(body) {
|
||||||
|
this.ctx.body = body
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMiddleware() {
|
||||||
|
return this.middleware(this.ctx, this.next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resourceId middleware", () => {
|
||||||
|
it("calls next() when there is no request object to parse", () => {
|
||||||
|
const config = new TestConfiguration(paramResource("main"))
|
||||||
|
|
||||||
|
config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
expect(config.ctx.resourceId).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates a resourceId middleware for context query parameters", () => {
|
||||||
|
const config = new TestConfiguration(paramResource("main"))
|
||||||
|
config.setParams({
|
||||||
|
main: "test"
|
||||||
|
})
|
||||||
|
|
||||||
|
config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.resourceId).toEqual("test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates a resourceId middleware for context query sub parameters", () => {
|
||||||
|
const config = new TestConfiguration(paramSubResource("main", "sub"))
|
||||||
|
config.setParams({
|
||||||
|
main: "main",
|
||||||
|
sub: "test"
|
||||||
|
})
|
||||||
|
|
||||||
|
config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.resourceId).toEqual("main")
|
||||||
|
expect(config.ctx.subResourceId).toEqual("test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates a resourceId middleware for context request body", () => {
|
||||||
|
const config = new TestConfiguration(bodyResource("main"))
|
||||||
|
config.setBody({
|
||||||
|
main: "test"
|
||||||
|
})
|
||||||
|
|
||||||
|
config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.resourceId).toEqual("test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generates a resourceId middleware for context request body sub fields", () => {
|
||||||
|
const config = new TestConfiguration(bodySubResource("main", "sub"))
|
||||||
|
config.setBody({
|
||||||
|
main: "main",
|
||||||
|
sub: "test"
|
||||||
|
})
|
||||||
|
|
||||||
|
config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.resourceId).toEqual("main")
|
||||||
|
expect(config.ctx.subResourceId).toEqual("test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("parses resourceIds correctly for custom middlewares", () => {
|
||||||
|
const middleware = new ResourceIdGetter("body")
|
||||||
|
.mainResource("custom")
|
||||||
|
.subResource("customSub")
|
||||||
|
.build()
|
||||||
|
config = new TestConfiguration(middleware)
|
||||||
|
config.setBody({
|
||||||
|
custom: "test",
|
||||||
|
customSub: "subtest"
|
||||||
|
})
|
||||||
|
|
||||||
|
config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.ctx.resourceId).toEqual("test")
|
||||||
|
expect(config.ctx.subResourceId).toEqual("subtest")
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,75 @@
|
||||||
|
const selfHostMiddleware = require("../selfhost");
|
||||||
|
const env = require("../../environment")
|
||||||
|
const hosting = require("../../utilities/builder/hosting");
|
||||||
|
jest.mock("../../environment")
|
||||||
|
jest.mock("../../utilities/builder/hosting")
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
constructor() {
|
||||||
|
this.next = jest.fn()
|
||||||
|
this.throw = jest.fn()
|
||||||
|
this.middleware = selfHostMiddleware
|
||||||
|
|
||||||
|
this.ctx = {
|
||||||
|
next: this.next,
|
||||||
|
throw: this.throw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMiddleware() {
|
||||||
|
return this.middleware(this.ctx, this.next)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCloudHosted() {
|
||||||
|
env.CLOUD = 1
|
||||||
|
env.SELF_HOSTED = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelfHosted() {
|
||||||
|
env.CLOUD = 0
|
||||||
|
env.SELF_HOSTED = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach() {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Self host middleware", () => {
|
||||||
|
let config
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.afterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls next() when CLOUD and SELF_HOSTED env vars are set", async () => {
|
||||||
|
env.CLOUD = 1
|
||||||
|
env.SELF_HOSTED = 1
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws when hostingInfo type is cloud", async () => {
|
||||||
|
config.setSelfHosted()
|
||||||
|
|
||||||
|
hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.CLOUD }))
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(400, "Endpoint unavailable in cloud hosting.")
|
||||||
|
expect(config.next).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls the self hosting middleware to pass through to next() when the hostingInfo type is self", async () => {
|
||||||
|
config.setSelfHosted()
|
||||||
|
|
||||||
|
hosting.getHostingInfo.mockImplementationOnce(() => ({ type: hosting.HostingTypes.SELF }))
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,129 @@
|
||||||
|
const usageQuotaMiddleware = require("../usageQuota")
|
||||||
|
const usageQuota = require("../../utilities/usageQuota")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const env = require("../../environment")
|
||||||
|
|
||||||
|
jest.mock("../../db");
|
||||||
|
jest.mock("../../utilities/usageQuota")
|
||||||
|
jest.mock("../../environment")
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
constructor() {
|
||||||
|
this.throw = jest.fn()
|
||||||
|
this.next = jest.fn()
|
||||||
|
this.middleware = usageQuotaMiddleware
|
||||||
|
this.ctx = {
|
||||||
|
throw: this.throw,
|
||||||
|
next: this.next,
|
||||||
|
user: {
|
||||||
|
appId: "test"
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
body: {}
|
||||||
|
},
|
||||||
|
req: {
|
||||||
|
method: "POST",
|
||||||
|
url: "/rows"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMiddleware() {
|
||||||
|
return this.middleware(this.ctx, this.next)
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudHosted(bool) {
|
||||||
|
if (bool) {
|
||||||
|
env.CLOUD = 1
|
||||||
|
this.ctx.auth = { apiKey: "test" }
|
||||||
|
} else {
|
||||||
|
env.CLOUD = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMethod(method) {
|
||||||
|
this.ctx.req.method = method
|
||||||
|
}
|
||||||
|
|
||||||
|
setUrl(url) {
|
||||||
|
this.ctx.req.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
setBody(body) {
|
||||||
|
this.ctx.request.body = body
|
||||||
|
}
|
||||||
|
|
||||||
|
setFiles(files) {
|
||||||
|
this.ctx.request.files = { file: files }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("usageQuota middleware", () => {
|
||||||
|
let config
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("skips the middleware if there is no usage property or method", async () => {
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("passes through to next middleware if document already exists", async () => {
|
||||||
|
config.setBody({
|
||||||
|
_id: "test"
|
||||||
|
})
|
||||||
|
|
||||||
|
CouchDB.mockImplementationOnce(() => ({
|
||||||
|
get: async () => true
|
||||||
|
}))
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
expect(config.ctx.preExisting).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if request has _id, but the document no longer exists", async () => {
|
||||||
|
config.setBody({
|
||||||
|
_id: "123"
|
||||||
|
})
|
||||||
|
|
||||||
|
CouchDB.mockImplementationOnce(() => ({
|
||||||
|
get: async () => {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
expect(config.throw).toHaveBeenCalledWith(404, `${config.ctx.request.body._id} does not exist`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calculates and persists the correct usage quota for the relevant action", async () => {
|
||||||
|
config.setUrl("/rows")
|
||||||
|
config.cloudHosted(true)
|
||||||
|
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(usageQuota.update).toHaveBeenCalledWith("test", "rows", 1)
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calculates the correct file size from a file upload call and adds it to quota", async () => {
|
||||||
|
config.setUrl("/upload")
|
||||||
|
config.cloudHosted(true)
|
||||||
|
config.setFiles([
|
||||||
|
{
|
||||||
|
size: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
size: 10000
|
||||||
|
},
|
||||||
|
])
|
||||||
|
await config.executeMiddleware()
|
||||||
|
|
||||||
|
expect(usageQuota.update).toHaveBeenCalledWith("test", "storage", 10100)
|
||||||
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
|
@ -43,6 +43,7 @@ module.exports = async (ctx, next) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if running in builder or a self hosted cloud usage quotas should not be executed
|
// if running in builder or a self hosted cloud usage quotas should not be executed
|
||||||
if (!env.CLOUD || env.SELF_HOSTED) {
|
if (!env.CLOUD || env.SELF_HOSTED) {
|
||||||
return next()
|
return next()
|
||||||
|
|
Loading…
Reference in New Issue