server: add basic is-admin tests and bug fixes
This commit is contained in:
parent
06d847dfa3
commit
e5b9b65b55
9 changed files with 182 additions and 106 deletions
|
@ -49,9 +49,9 @@ const ExpirationBadge = ({
|
|||
return (
|
||||
<Badge type={isExpired ? "error" : "warning"}>
|
||||
<Tooltip
|
||||
hideArrow
|
||||
text={`${expirationDate.toLocaleDateString()} ${expirationDate.toLocaleTimeString()}`}>
|
||||
{isExpired ? "Expired" : `Expires ${timeUntilString}`}
|
||||
hideArrow
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
)
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import * as request from 'supertest'
|
||||
import { app } from '../app'
|
||||
import * as request from "supertest"
|
||||
import { app } from "../app"
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('should return 200 and a status up', (done) => {
|
||||
request(app)
|
||||
.get(`/health`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end((err, res) => {
|
||||
if (err) return done(err)
|
||||
expect(res.body).toMatchObject({ 'status': 'UP' })
|
||||
done()
|
||||
})
|
||||
})
|
||||
describe("GET /health", () => {
|
||||
it("should return 200 and a status up", (done) => {
|
||||
request(app)
|
||||
.get(`/health`)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
.end((err, res) => {
|
||||
if (err) return done(err)
|
||||
expect(res.body).toMatchObject({ status: "UP" })
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,62 +1,64 @@
|
|||
import { config } from "../config"
|
||||
|
||||
describe("Config", () => {
|
||||
it("should build a valid development config when no environment is set", () => {
|
||||
const emptyEnv = {};
|
||||
const result = config(emptyEnv);
|
||||
it("should build a valid development config when no environment is set", () => {
|
||||
const emptyEnv = {}
|
||||
const result = config(emptyEnv)
|
||||
|
||||
expect(result).toHaveProperty("is_production", false)
|
||||
expect(result).toHaveProperty("port")
|
||||
expect(result).toHaveProperty("jwt_secret")
|
||||
expect(result).toHaveProperty("drift_home")
|
||||
expect(result).toHaveProperty("memory_db")
|
||||
expect(result).toHaveProperty("enable_admin")
|
||||
expect(result).toHaveProperty("secret_key")
|
||||
expect(result).toHaveProperty("registration_password")
|
||||
expect(result).toHaveProperty("welcome_content")
|
||||
expect(result).toHaveProperty("welcome_title")
|
||||
})
|
||||
expect(result).toHaveProperty("is_production", false)
|
||||
expect(result).toHaveProperty("port")
|
||||
expect(result).toHaveProperty("jwt_secret")
|
||||
expect(result).toHaveProperty("drift_home")
|
||||
expect(result).toHaveProperty("memory_db")
|
||||
expect(result).toHaveProperty("enable_admin")
|
||||
expect(result).toHaveProperty("secret_key")
|
||||
expect(result).toHaveProperty("registration_password")
|
||||
expect(result).toHaveProperty("welcome_content")
|
||||
expect(result).toHaveProperty("welcome_title")
|
||||
})
|
||||
|
||||
it("should fail when building a prod environment without SECRET_KEY", () => {
|
||||
expect(() => config({ NODE_ENV: "production" }))
|
||||
.toThrow(new Error("Missing environment variable: SECRET_KEY"))
|
||||
})
|
||||
it("should fail when building a prod environment without SECRET_KEY", () => {
|
||||
expect(() => config({ NODE_ENV: "production" })).toThrow(
|
||||
new Error("Missing environment variable: SECRET_KEY")
|
||||
)
|
||||
})
|
||||
|
||||
it("should build a prod config with a SECRET_KEY", () => {
|
||||
const result = config({ NODE_ENV: "production", SECRET_KEY: "secret" })
|
||||
it("should build a prod config with a SECRET_KEY", () => {
|
||||
const result = config({ NODE_ENV: "production", SECRET_KEY: "secret" })
|
||||
|
||||
expect(result).toHaveProperty("is_production", true)
|
||||
expect(result).toHaveProperty("secret_key", "secret")
|
||||
})
|
||||
expect(result).toHaveProperty("is_production", true)
|
||||
expect(result).toHaveProperty("secret_key", "secret")
|
||||
})
|
||||
|
||||
describe("jwt_secret", () => {
|
||||
it("should use default jwt_secret when environment is blank string", () => {
|
||||
const result = config({ JWT_SECRET: "" })
|
||||
describe("jwt_secret", () => {
|
||||
it("should use default jwt_secret when environment is blank string", () => {
|
||||
const result = config({ JWT_SECRET: "" })
|
||||
|
||||
expect(result).toHaveProperty("is_production", false)
|
||||
expect(result).toHaveProperty("jwt_secret", "myjwtsecret")
|
||||
})
|
||||
})
|
||||
expect(result).toHaveProperty("is_production", false)
|
||||
expect(result).toHaveProperty("jwt_secret", "myjwtsecret")
|
||||
})
|
||||
})
|
||||
|
||||
describe("booleans", () => {
|
||||
it("should parse 'true' as true", () => {
|
||||
const result = config({ MEMORY_DB: "true" })
|
||||
describe("booleans", () => {
|
||||
it("should parse 'true' as true", () => {
|
||||
const result = config({ MEMORY_DB: "true" })
|
||||
|
||||
expect(result).toHaveProperty("memory_db", true)
|
||||
})
|
||||
it("should parse 'false' as false", () => {
|
||||
const result = config({ MEMORY_DB: "false" })
|
||||
expect(result).toHaveProperty("memory_db", true)
|
||||
})
|
||||
it("should parse 'false' as false", () => {
|
||||
const result = config({ MEMORY_DB: "false" })
|
||||
|
||||
expect(result).toHaveProperty("memory_db", false)
|
||||
})
|
||||
it("should fail when it is not parseable", () => {
|
||||
expect(() => config({ MEMORY_DB: "foo" }))
|
||||
.toThrow(new Error("Invalid boolean value: foo"))
|
||||
})
|
||||
it("should default to false when the string is empty", () => {
|
||||
const result = config({ MEMORY_DB: "" })
|
||||
expect(result).toHaveProperty("memory_db", false)
|
||||
})
|
||||
it("should fail when it is not parseable", () => {
|
||||
expect(() => config({ MEMORY_DB: "foo" })).toThrow(
|
||||
new Error("Invalid boolean value: foo")
|
||||
)
|
||||
})
|
||||
it("should default to false when the string is empty", () => {
|
||||
const result = config({ MEMORY_DB: "" })
|
||||
|
||||
expect(result).toHaveProperty("memory_db", false)
|
||||
})
|
||||
})
|
||||
expect(result).toHaveProperty("memory_db", false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
17
server/src/lib/__tests__/get-html-from-drift-file.ts
Normal file
17
server/src/lib/__tests__/get-html-from-drift-file.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import getHtmlFromFile from "@lib/get-html-from-drift-file"
|
||||
|
||||
describe("get-html-from-drift-file", () => {
|
||||
it("should not wrap markdown in code blocks", () => {
|
||||
const markdown = `## My Markdown`
|
||||
const html = getHtmlFromFile({ content: markdown, title: "my-markdown.md" })
|
||||
// the string is <h2><a href=\"#my-markdown\" id=\"my-markdown\" style=\"color:inherit\">My Markdown</a></h2>,
|
||||
// but we dont wan't to be too strict in case markup changes
|
||||
expect(html).toMatch(/<h2><a.*<\/a><\/h2>/)
|
||||
})
|
||||
|
||||
it("should wrap code in code blocks", () => {
|
||||
const code = `const foo = "bar"`
|
||||
const html = getHtmlFromFile({ content: code, title: "my-code.js" })
|
||||
expect(html).toMatch(/<pre><code class="prism-code language-js">/)
|
||||
})
|
||||
})
|
50
server/src/lib/__tests__/middleware/is-admin.ts
Normal file
50
server/src/lib/__tests__/middleware/is-admin.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
// import * as request from 'supertest'
|
||||
// import { app } from '../../../app'
|
||||
import { NextFunction, Response } from "express"
|
||||
import isAdmin from "@lib/middleware/is-admin"
|
||||
import { UserJwtRequest } from "@lib/middleware/jwt"
|
||||
|
||||
describe("is-admin middlware", () => {
|
||||
let mockRequest: Partial<UserJwtRequest>
|
||||
let mockResponse: Partial<Response>
|
||||
let nextFunction: NextFunction = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {}
|
||||
mockResponse = {
|
||||
sendStatus: jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
it("should return 401 if no authorization header", async () => {
|
||||
const res = mockResponse as Response
|
||||
isAdmin(mockRequest as UserJwtRequest, res, nextFunction)
|
||||
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
||||
})
|
||||
|
||||
it("should return 401 if no token is supplied", async () => {
|
||||
const req = mockRequest as UserJwtRequest
|
||||
req.headers = {
|
||||
authorization: "Bearer"
|
||||
}
|
||||
isAdmin(req, mockResponse as Response, nextFunction)
|
||||
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
||||
})
|
||||
|
||||
it("should return 404 if config.enable_admin is false", async () => {
|
||||
jest.mock("../../config", () => ({
|
||||
enable_admin: false
|
||||
}))
|
||||
|
||||
const req = mockRequest as UserJwtRequest
|
||||
req.headers = {
|
||||
authorization: "Bearer 123"
|
||||
}
|
||||
isAdmin(req, mockResponse as Response, nextFunction)
|
||||
expect(mockResponse.sendStatus).toBeCalledWith(404)
|
||||
})
|
||||
|
||||
// TODO: 403 if !isAdmin
|
||||
// Verify it calls next() if admin
|
||||
// Requires mocking config.enable_admin
|
||||
})
|
|
@ -6,12 +6,12 @@ type Config = {
|
|||
memory_db: boolean
|
||||
enable_admin: boolean
|
||||
secret_key: string
|
||||
registration_password: string,
|
||||
welcome_content: string | undefined,
|
||||
welcome_title: string | undefined,
|
||||
registration_password: string
|
||||
welcome_content: string | undefined
|
||||
welcome_title: string | undefined
|
||||
}
|
||||
|
||||
type EnvironmentValue = string | undefined;
|
||||
type EnvironmentValue = string | undefined
|
||||
type Environment = { [key: string]: EnvironmentValue }
|
||||
|
||||
export const config = (env: Environment): Config => {
|
||||
|
@ -34,7 +34,10 @@ export const config = (env: Environment): Config => {
|
|||
return str
|
||||
}
|
||||
|
||||
const defaultIfUndefined = (str: EnvironmentValue, defaultValue: string): string => {
|
||||
const defaultIfUndefined = (
|
||||
str: EnvironmentValue,
|
||||
defaultValue: string
|
||||
): string => {
|
||||
if (str === undefined) {
|
||||
return defaultValue
|
||||
}
|
||||
|
@ -52,11 +55,15 @@ export const config = (env: Environment): Config => {
|
|||
}
|
||||
}
|
||||
|
||||
const is_production = env.NODE_ENV === "production";
|
||||
const is_production = env.NODE_ENV === "production"
|
||||
|
||||
const developmentDefault = (str: EnvironmentValue, name: string, defaultValue: string): string => {
|
||||
if (is_production) return throwIfUndefined(str, name);
|
||||
return defaultIfUndefined(str, defaultValue);
|
||||
const developmentDefault = (
|
||||
str: EnvironmentValue,
|
||||
name: string,
|
||||
defaultValue: string
|
||||
): string => {
|
||||
if (is_production) return throwIfUndefined(str, name)
|
||||
return defaultIfUndefined(str, defaultValue)
|
||||
}
|
||||
|
||||
validNodeEnvs(env.NODE_ENV)
|
||||
|
@ -72,7 +79,6 @@ export const config = (env: Environment): Config => {
|
|||
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
||||
welcome_content: env.WELCOME_CONTENT,
|
||||
welcome_title: env.WELCOME_TITLE
|
||||
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
|
|
@ -5,37 +5,35 @@ import { File } from "@lib/models/File"
|
|||
* returns rendered HTML from a Drift file
|
||||
*/
|
||||
function getHtmlFromFile({ content, title }: Pick<File, "content" | "title">) {
|
||||
const renderAsMarkdown = [
|
||||
"markdown",
|
||||
"md",
|
||||
"mdown",
|
||||
"mkdn",
|
||||
"mkd",
|
||||
"mdwn",
|
||||
"mdtxt",
|
||||
"mdtext",
|
||||
"text",
|
||||
""
|
||||
]
|
||||
const fileType = () => {
|
||||
const pathParts = title.split(".")
|
||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||
return language
|
||||
}
|
||||
const type = fileType()
|
||||
let contentToRender: string = content || ""
|
||||
const renderAsMarkdown = [
|
||||
"markdown",
|
||||
"md",
|
||||
"mdown",
|
||||
"mkdn",
|
||||
"mkd",
|
||||
"mdwn",
|
||||
"mdtxt",
|
||||
"mdtext",
|
||||
"text",
|
||||
""
|
||||
]
|
||||
const fileType = () => {
|
||||
const pathParts = title.split(".")
|
||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||
return language
|
||||
}
|
||||
const type = fileType()
|
||||
let contentToRender: string = content || ""
|
||||
|
||||
if (!renderAsMarkdown.includes(type)) {
|
||||
contentToRender = `~~~${type}
|
||||
if (!renderAsMarkdown.includes(type)) {
|
||||
contentToRender = `~~~${type}
|
||||
${content}
|
||||
~~~`
|
||||
} else {
|
||||
contentToRender = "\n" + content
|
||||
}
|
||||
console.log(contentToRender.slice(0, 50))
|
||||
const html = markdown(contentToRender)
|
||||
return html
|
||||
} else {
|
||||
contentToRender = "\n" + content
|
||||
}
|
||||
const html = markdown(contentToRender)
|
||||
return html
|
||||
}
|
||||
|
||||
|
||||
export default getHtmlFromFile
|
|
@ -11,16 +11,20 @@ export interface UserJwtRequest extends Request {
|
|||
user?: User
|
||||
}
|
||||
|
||||
export default function authenticateToken(
|
||||
export default function isAdmin(
|
||||
req: UserJwtRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (!req.headers?.authorization) {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
const authHeader = req.headers["authorization"]
|
||||
const token = authHeader && authHeader.split(" ")[1]
|
||||
if (token == null) return res.sendStatus(401)
|
||||
if (!token) return res.sendStatus(401)
|
||||
console.log(config)
|
||||
if (!config.enable_admin) return res.sendStatus(404)
|
||||
|
||||
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
||||
if (err) return res.sendStatus(403)
|
||||
const userObj = await UserModel.findByPk(user.id, {
|
||||
|
|
|
@ -357,4 +357,3 @@ posts.delete("/:id", jwt, async (req: UserJwtRequest, res, next) => {
|
|||
next(e)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue