server: add basic is-admin tests and bug fixes

This commit is contained in:
Max Leiter 2022-04-05 16:17:08 -07:00
parent 06d847dfa3
commit e5b9b65b55
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
9 changed files with 182 additions and 106 deletions

View file

@ -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>
)

View file

@ -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()
})
})
})

View file

@ -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)
})
})
})

View 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">/)
})
})

View 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
})

View file

@ -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
}

View file

@ -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
export default getHtmlFromFile

View file

@ -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, {

View file

@ -357,4 +357,3 @@ posts.delete("/:id", jwt, async (req: UserJwtRequest, res, next) => {
next(e)
}
})