Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: project auto provisioning #267

Merged
merged 12 commits into from
Oct 16, 2023
15 changes: 15 additions & 0 deletions migrations/1694620030174_global-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable camelcase */
exports.up = pgm => {
pgm.createTable({ schema: "jtl", name: "global_settings" }, {
id: {
type: "serial",
},
project_auto_provisioning: {
type: "boolean",
"default": false,
notNull: true,
},
})

}

13 changes: 13 additions & 0 deletions migrations/1694620617193_global-settings-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
exports.up = async pgm => {
try {

await pgm.db.query({
text: `INSERT INTO jtl.global_settings (project_auto_provisioning)
values ($1)`,
values: [false],
})
} catch(error) {
console.log(error)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { getGlobalSettingsController } from "./get-global-settings-controller"
import { Response } from "express"
import { AllowedRoles } from "../../middleware/authorization-middleware"
import { IGetUserAuthInfoRequest } from "../../middleware/request.model"
import { db } from "../../../db/db"

jest.mock("../../../db/db")
const mockResponse = () => {
const res: Partial<Response> = {}
res.json = jest.fn().mockReturnValue(res)
res.status = jest.fn().mockReturnValue(res)
return res
}
describe("getGlobalSettingsController", () => {
it("should return global settings", async () => {
const response = mockResponse()
const querySpy = jest.spyOn(require("../../queries/global-settings"), "getGlobalSettings")
const request = {
user: {
role: AllowedRoles.Admin,
},
}
db.one = jest.fn().mockReturnValueOnce({ project_auto_provisioning: true })
await getGlobalSettingsController(
request as unknown as IGetUserAuthInfoRequest,
response as unknown as Response)
expect(querySpy).toHaveBeenCalledTimes(1)
expect(response.json).toHaveBeenCalledTimes(1)
expect(response.json).toHaveBeenCalledWith({ projectAutoProvisioning: true })
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IGetUserAuthInfoRequest } from "../../middleware/request.model"
import { Response } from "express"
import { db } from "../../../db/db"
import { getGlobalSettings } from "../../queries/global-settings"
import { StatusCode } from "../../utils/status-code"

export const getGlobalSettingsController = async (req: IGetUserAuthInfoRequest, res: Response) => {
const globalSettings = await db.one(getGlobalSettings())
res.status(StatusCode.Ok).json({
projectAutoProvisioning: globalSettings.project_auto_provisioning,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { db } from "../../../db/db"
import { AllowedRoles } from "../../middleware/authorization-middleware"
import { IGetUserAuthInfoRequest } from "../../middleware/request.model"
import { Response } from "express"
import { updateGlobalSettingsController } from "./update-global-settings-controller"

jest.mock("../../../db/db")
const mockResponse = () => {
const res: Partial<Response> = {}
res.send = jest.fn().mockReturnValue(res)
res.status = jest.fn().mockReturnValue(res)
return res
}

describe("updateGlobalSettingsController", () => {
it("should save the new settings into database", async () => {
const response = mockResponse()
const querySpy = jest.spyOn(require("../../queries/global-settings"), "updateGlobalSettings")
const request = {
user: {
role: AllowedRoles.Admin,
},
body: {
projectAutoProvisioning: true,
},
}
db.none = jest.fn().mockResolvedValueOnce(null)

await updateGlobalSettingsController(
request as unknown as IGetUserAuthInfoRequest,
response as unknown as Response)
expect(querySpy).toHaveBeenCalledTimes(1)
expect(querySpy).toHaveBeenCalledWith(true)
expect(response.send).toHaveBeenCalledTimes(1)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IGetUserAuthInfoRequest } from "../../middleware/request.model"
import { Response } from "express"
import { updateGlobalSettings } from "../../queries/global-settings"
import { db } from "../../../db/db"
import { StatusCode } from "../../utils/status-code"

export const updateGlobalSettingsController = async (req: IGetUserAuthInfoRequest, res: Response) => {
const updatedSettings = req.body
await db.none(updateGlobalSettings(updatedSettings.projectAutoProvisioning))
return res.status(StatusCode.NoContent).send()

}
51 changes: 50 additions & 1 deletion src/server/middleware/project-exists-middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IGetUserAuthInfoRequest } from "./request.model"
import { Response, NextFunction } from "express"
import { projectExistsMiddleware } from "./project-exists-middleware"
import { projectAutoProvisioningMiddleware, projectExistsMiddleware } from "./project-exists-middleware"
import { db } from "../../db/db"
import Boom = require("boom")

Expand Down Expand Up @@ -31,5 +31,54 @@ describe("projectExistsMiddleware", () => {
expect(nextFunction).toHaveBeenCalledTimes(1)
expect(nextFunction).toHaveBeenCalledWith()
})
})


describe("projectAutoProvisioningMiddleware", () => {
const nextFunction: NextFunction = jest.fn()


beforeEach(() => {
jest.resetAllMocks()
})

it("should create new project when it does not exists and auto provisioning is enabled in global settings",
async () => {
const request: any = { params: { projectName: "does not exist" } }
db.oneOrNone = jest.fn().mockReturnValueOnce(null)
db.one = jest.fn().mockReturnValueOnce({ project_auto_provisioning: true })
const spy = jest.spyOn(require("../queries/projects"), "createNewProject")
await projectAutoProvisioningMiddleware(request as unknown as IGetUserAuthInfoRequest,
{} as unknown as Response, nextFunction)
expect(nextFunction).toHaveBeenCalledTimes(1)
expect(nextFunction).toHaveBeenCalledWith()
expect(spy).toHaveBeenCalledWith("does not exist", true)
})


it("should continue when project exists",
async () => {
const request: any = { params: { projectName: "does not exist" } }
db.oneOrNone = jest.fn().mockReturnValueOnce("project data")
const spy = jest.spyOn(require("../queries/global-settings"), "getGlobalSettings")
await projectAutoProvisioningMiddleware(request as unknown as IGetUserAuthInfoRequest,
{} as unknown as Response, nextFunction)
expect(nextFunction).toHaveBeenCalledTimes(1)
expect(nextFunction).toHaveBeenCalledWith()
expect(spy).toHaveBeenCalledTimes(0)
})

it("should return 404 when project does not exists and auto provisioning is disabled in global settings",
async () => {
const request: any = { params: { projectName: "does not exist" } }
db.oneOrNone = jest.fn().mockReturnValueOnce(null)
db.one = jest.fn().mockReturnValueOnce({ project_auto_provisioning: false })
const spy = jest.spyOn(require("../queries/projects"), "createNewProject")
await projectAutoProvisioningMiddleware(request as unknown as IGetUserAuthInfoRequest,
{} as unknown as Response, nextFunction)
expect(nextFunction).toHaveBeenCalledTimes(1)
expect(nextFunction).toHaveBeenCalledWith(Boom.notFound("Project not found"))
expect(spy).toHaveBeenCalledTimes(0)
})

})
20 changes: 19 additions & 1 deletion src/server/middleware/project-exists-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { IGetUserAuthInfoRequest } from "./request.model"
import { NextFunction, Response } from "express"
import { db } from "../../db/db"
import * as boom from "boom"
import { findProjectId } from "../queries/projects"
import { createNewProject, findProjectId } from "../queries/projects"
import { getGlobalSettings } from "../queries/global-settings"
import { logger } from "../../logger"

export const projectExistsMiddleware = async (req: IGetUserAuthInfoRequest, res: Response, next: NextFunction) => {

Expand All @@ -13,3 +15,19 @@ export const projectExistsMiddleware = async (req: IGetUserAuthInfoRequest, res:
}
next()
}

export const projectAutoProvisioningMiddleware = async (
req: IGetUserAuthInfoRequest, res: Response, next: NextFunction) => {
const { projectName } = req.params
const project = await db.oneOrNone(findProjectId(projectName))
if (project) {
return next()
}
const globalSettings = await db.one(getGlobalSettings())
if (globalSettings.project_auto_provisioning === true) {
logger.info(`Project auto-provisioning is enabled, creating a new project ${projectName}`)
await db.one(createNewProject(projectName, true))
return next()
}
return next(boom.notFound("Project not found"))
}
12 changes: 12 additions & 0 deletions src/server/queries/global-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const getGlobalSettings = () => {
return {
text: `SELECT * FROM jtl.global_settings`,
}
}

export const updateGlobalSettings = (projectAutoprovisioning: boolean) => {
return {
text: "UPDATE jtl.global_settings SET project_auto_provisioning = $1",
values: [projectAutoprovisioning],
}
}
6 changes: 3 additions & 3 deletions src/server/queries/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ export const isExistingProject = projectName => {
}
}

export const createNewProject = projectName => {
export const createNewProject = (projectName, upsertScenario = false) => {
return {
name: "create-new-project",
text: "INSERT INTO jtl.projects(project_name) VALUES($1) RETURNING id",
values: [projectName],
text: "INSERT INTO jtl.projects(project_name, upsert_scenario) VALUES($1, $2) RETURNING id",
values: [projectName, upsertScenario],
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { ApiTokensRoutes } from "./routes/api-token"
import { UsersRoutes } from "./routes/users"
import { InitRoutes } from "./routes/init"
import { GlobalSettings } from "./routes/global-settings"

Check warning on line 10 in src/server/router.ts

View check run for this annotation

Codecov / codecov/patch

src/server/router.ts#L10

Added line #L10 was not covered by tests
const env = process.env.ENVIRONMENT

export class Router {
Expand All @@ -19,6 +20,7 @@
private userRoutes: UsersRoutes
private testDataSetup: TestDataSetup
private initRoutes: InitRoutes
private globalSettings: GlobalSettings
constructor() {
this.projectRoutes = new ProjectRoutes()
this.scenarioRoutes = new ScenarioRoutes()
Expand All @@ -29,6 +31,7 @@
this.userRoutes = new UsersRoutes()
this.testDataSetup = new TestDataSetup()
this.initRoutes = new InitRoutes()
this.globalSettings = new GlobalSettings()

Check warning on line 34 in src/server/router.ts

View check run for this annotation

Codecov / codecov/patch

src/server/router.ts#L34

Added line #L34 was not covered by tests
}

getRoutes(app) {
Expand All @@ -43,5 +46,6 @@
this.testDataSetup.routes(app)
}
this.initRoutes.routes(app)
this.globalSettings.routes(app)

Check warning on line 49 in src/server/router.ts

View check run for this annotation

Codecov / codecov/patch

src/server/router.ts#L49

Added line #L49 was not covered by tests
}
}
28 changes: 28 additions & 0 deletions src/server/routes/global-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as express from "express"
import { authenticationMiddleware } from "../middleware/authentication-middleware"
import { AllowedRoles, authorizationMiddleware } from "../middleware/authorization-middleware"
import { wrapAsync } from "../errors/error-handler"

Check warning on line 4 in src/server/routes/global-settings.ts

View check run for this annotation

Codecov / codecov/patch

src/server/routes/global-settings.ts#L2-L4

Added lines #L2 - L4 were not covered by tests
import { IGetUserAuthInfoRequest } from "../middleware/request.model"
import { Response } from "express"
import { getGlobalSettingsController } from "../controllers/global-settings/get-global-settings-controller"
import { updateGlobalSettingsController } from "../controllers/global-settings/update-global-settings-controller"
import { bodySchemaValidator } from "../schema-validator/schema-validator-middleware"
import { globalSettingsBodySchema } from "../schema-validator/global-settings"

Check warning on line 10 in src/server/routes/global-settings.ts

View check run for this annotation

Codecov / codecov/patch

src/server/routes/global-settings.ts#L7-L10

Added lines #L7 - L10 were not covered by tests

export class GlobalSettings {

Check warning on line 12 in src/server/routes/global-settings.ts

View check run for this annotation

Codecov / codecov/patch

src/server/routes/global-settings.ts#L12

Added line #L12 was not covered by tests
routes(app: express.Application): void {
app.route("/api/global-settings")

Check warning on line 14 in src/server/routes/global-settings.ts

View check run for this annotation

Codecov / codecov/patch

src/server/routes/global-settings.ts#L14

Added line #L14 was not covered by tests
.get(
authenticationMiddleware,
authorizationMiddleware([AllowedRoles.Admin]),
wrapAsync((req: IGetUserAuthInfoRequest, res: Response) => getGlobalSettingsController(req, res))

Check warning on line 18 in src/server/routes/global-settings.ts

View check run for this annotation

Codecov / codecov/patch

src/server/routes/global-settings.ts#L18

Added line #L18 was not covered by tests
)

.put(
authenticationMiddleware,
authorizationMiddleware([AllowedRoles.Admin]),
bodySchemaValidator(globalSettingsBodySchema),
wrapAsync((req: IGetUserAuthInfoRequest, res: Response) => updateGlobalSettingsController(req, res))

Check warning on line 25 in src/server/routes/global-settings.ts

View check run for this annotation

Codecov / codecov/patch

src/server/routes/global-settings.ts#L25

Added line #L25 was not covered by tests
)
}
}
4 changes: 2 additions & 2 deletions src/server/routes/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import { AllowedRoles, authorizationMiddleware } from "../middleware/authorization-middleware"
import { authenticationMiddleware } from "../middleware/authentication-middleware"
import { getRequestStatsExportController } from "../controllers/item/get-request-stats-export-controller"
import { projectExistsMiddleware } from "../middleware/project-exists-middleware"
import { projectAutoProvisioningMiddleware, projectExistsMiddleware } from "../middleware/project-exists-middleware"

Check warning on line 36 in src/server/routes/item.ts

View check run for this annotation

Codecov / codecov/patch

src/server/routes/item.ts#L36

Added line #L36 was not covered by tests

export class ItemsRoutes {

Expand All @@ -52,7 +52,7 @@
authenticationMiddleware,
authorizationMiddleware([AllowedRoles.Operator, AllowedRoles.Admin]),
paramsSchemaValidator(newItemParamSchema),
projectExistsMiddleware,
projectAutoProvisioningMiddleware,
createItemController)

app.route("/api/projects/:projectName/scenarios/:scenarioName/items/start-async")
Expand Down
6 changes: 6 additions & 0 deletions src/server/schema-validator/global-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as Joi from "joi"


export const globalSettingsBodySchema = {
projectAutoProvisioning: Joi.boolean().required(),
}
4 changes: 2 additions & 2 deletions src/tests/integration/api-tokens.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("Api tokens", () => {
})

describe("GET /api-tokens", () => {
it("should not be able to get api tokens as unathorized user", async () => {
it("should not be able to get api tokens as unauthorized user", async () => {
await request(__server__)
.get(routes.apiTokens)
.send()
Expand All @@ -71,7 +71,7 @@ describe("Api tokens", () => {
tokenId = body[0].id
})
})
it("should not be able to delete api token as uanthorized user", async () => {
it("should not be able to delete api token as unauthorized user", async () => {
await request(__server__)
.delete(routes.apiTokens)
.send()
Expand Down
33 changes: 33 additions & 0 deletions src/tests/integration/global-settings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { userSetup } from "./helper/state"
import * as request from "supertest"
import { routes } from "./helper/routes"
import { StatusCode } from "../../server/utils/status-code"

describe("global settings", () => {
let credentials
beforeAll(async () => {
try {
({ data: credentials } = await userSetup())
} catch(error) {
console.log(error)
}
})
it("should return the global settings", async () => {
await request(__server__)
.get(routes.globalSettings)
.set(__tokenHeaderKey__, credentials.token)
.set("Accept", "application/json")
.send()
.expect(StatusCode.Ok, { projectAutoProvisioning: false })
})
it("should be able to change the global settings", async () => {
await request(__server__)
.put(routes.globalSettings)
.set(__tokenHeaderKey__, credentials.token)
.set("Accept", "application/json")
.send({
projectAutoProvisioning: true,
})
.expect(StatusCode.NoContent)
})
})
1 change: 1 addition & 0 deletions src/tests/integration/helper/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const routes = {
apiTokens: "/api/api-tokens",
users: "/api/users",
init: "/api/info",
globalSettings: "/api/global-settings",
}
Loading