Skip to content

Commit

Permalink
✨ add setup for api tests using DB
Browse files Browse the repository at this point in the history
  • Loading branch information
danyx23 committed Apr 10, 2024
1 parent 5fc6796 commit 506aa61
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 279 deletions.
72 changes: 70 additions & 2 deletions adminSiteServer/app.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
import { OwidAdminApp } from "./app.js"
import { OwidAdminApp } from "./appClass.js"
import { jest } from "@jest/globals"
import { logInAsUser } from "./authentication.js"
import knex, { Knex } from "knex"
import { dbTestConfig } from "../db/tests/dbTestConfig.js"
import sqlFixtures from "sql-fixtures"
import {
TransactionCloseMode,
knexReadWriteTransaction,
setKnexInstance,
} from "../db/db.js"
import { cleanTestDb } from "../db/tests/testHelpers.js"

jest.setTimeout(10000) // wait for up to 10s for the app server to start
let testKnexInstance: Knex<any, unknown[]> | undefined = undefined
let serverKnexInstance: Knex<any, unknown[]> | undefined = undefined

describe(OwidAdminApp, () => {
beforeAll(async () => {
const dataSpec = {
users: [
{
email: "[email protected]",
fullName: "Admin",
password: "admin",
createdAt: new Date(),
updatedAt: new Date(),
},
],
}
testKnexInstance = knex(dbTestConfig)
serverKnexInstance = knex(dbTestConfig)
await cleanTestDb(testKnexInstance)

const fixturesCreator = new sqlFixtures(testKnexInstance)
await fixturesCreator.create(dataSpec)
setKnexInstance(serverKnexInstance!)
})

afterAll((done: any) => {
// We leave the user in the database for other tests to use
// For other cases it is good to drop any rows created in the test
void Promise.allSettled([
testKnexInstance?.destroy(),
serverKnexInstance?.destroy(),
]).then(() => done())
})

describe("OwidAdminApp", () => {
const app = new OwidAdminApp({ isDev: true, gitCmsDir: "", quiet: true })

it("should be able to create an app", () => {
Expand All @@ -12,7 +54,33 @@ describe(OwidAdminApp, () => {

it("should be able to start the app", async () => {
await app.startListening(8765, "localhost")
console.error("Server started")
expect(app.server).toBeTruthy()
const _ = await knexReadWriteTransaction(
async (trx) => {

Check warning on line 60 in adminSiteServer/app.test.tsx

View workflow job for this annotation

GitHub Actions / eslint

'trx' is defined but never used. Allowed unused args must match /^_/u
console.error("Transaction started")
const cookieId = await logInAsUser({
email: "[email protected]",
id: 1,
})
console.error("Logged in")
const bla = await fetch(
"http://localhost:8765/admin/nodeVersion",
{
headers: { cookie: `sessionid=${cookieId.id}` },
}
)
console.error("fetched")
expect(bla.status).toBe(200)
const text = await bla.text()
console.error("text", text)
expect(text).toBe("v18.16.1")
},
TransactionCloseMode.Close,
testKnexInstance
)
console.error("Transaction done")
await app.stopListening()
console.error("Server stopped")
})
})
262 changes: 2 additions & 260 deletions adminSiteServer/app.tsx
Original file line number Diff line number Diff line change
@@ -1,268 +1,10 @@
import React from "react"
import { simpleGit } from "simple-git"
import express, { NextFunction } from "express"
require("express-async-errors") // todo: why the require?
import cookieParser from "cookie-parser"
import "reflect-metadata"
import http from "http"
import Bugsnag from "@bugsnag/js"
import BugsnagPluginExpress from "@bugsnag/plugin-express"
import { GIT_CMS_DIR } from "../gitCms/GitCmsConstants.js"
import {
ADMIN_SERVER_HOST,
ADMIN_SERVER_PORT,
BAKED_BASE_URL,
BUGSNAG_NODE_API_KEY,
ENV,
} from "../settings/serverSettings.js"
import * as db from "../db/db.js"
import * as wpdb from "../db/wpdb.js"
import { IndexPage } from "./IndexPage.js"
import {
authCloudflareSSOMiddleware,
authMiddleware,
} from "./authentication.js"
import { apiRouter } from "./apiRouter.js"
import { testPageRouter } from "./testPageRouter.js"
import { adminRouter } from "./adminRouter.js"
import { renderToHtmlPage } from "../serverUtils/serverUtil.js"

import { publicApiRouter } from "./publicApiRouter.js"
import { mockSiteRouter } from "./mockSiteRouter.js"
import { GIT_CMS_DIR } from "../gitCms/GitCmsConstants.js"
import { GdocsContentSource } from "@ourworldindata/utils"
import OwidGdocPage from "../site/gdocs/OwidGdocPage.js"
import { getAndLoadGdocById } from "../db/model/Gdoc/GdocFactory.js"

interface OwidAdminAppOptions {
gitCmsDir: string
isDev: boolean
quiet?: boolean
}

export class OwidAdminApp {
constructor(options: OwidAdminAppOptions) {
this.options = options
}

app = express()
private options: OwidAdminAppOptions

private async getGitCmsBranchName() {
const git = simpleGit({
baseDir: this.options.gitCmsDir,
binary: "git",
maxConcurrentProcesses: 1,
})
const branches = await git.branchLocal()
const gitCmsBranchName = await branches.current
return gitCmsBranchName
}

private gitCmsBranchName = ""

server?: http.Server
async stopListening() {
if (!this.server) return

this.server.close()
}

async startListening(adminServerPort: number, adminServerHost: string) {
this.gitCmsBranchName = await this.getGitCmsBranchName()
let bugsnagMiddleware

const { app } = this

if (BUGSNAG_NODE_API_KEY) {
Bugsnag.start({
apiKey: BUGSNAG_NODE_API_KEY,
context: "admin-server",
plugins: [BugsnagPluginExpress],
autoTrackSessions: false,
})
bugsnagMiddleware = Bugsnag.getPlugin("express")
// From the docs: "this must be the first piece of middleware in the
// stack. It can only capture errors in downstream middleware"
if (bugsnagMiddleware) app.use(bugsnagMiddleware.requestHandler)
}

// since the server is running behind a reverse proxy (nginx), we need to "trust"
// the X-Forwarded-For header in order to get the real request IP
// https://expressjs.com/en/guide/behind-proxies.html
app.set("trust proxy", true)

// Parse cookies https://github.com/expressjs/cookie-parser
app.use(cookieParser())

app.use(express.urlencoded({ extended: true, limit: "50mb" }))

app.use("/admin/login", authCloudflareSSOMiddleware)

// Require authentication (only for /admin requests)
app.use(authMiddleware)

app.use("/assets", express.static("dist/assets"))
app.use("/fonts", express.static("public/fonts"))

app.use("/api", publicApiRouter.router)
app.use("/assets-admin", express.static("dist/assets-admin"))
app.use("/admin/api", apiRouter.router)
app.use("/admin/test", testPageRouter)
app.use("/admin/storybook", express.static(".storybook/build"))
app.use("/admin", adminRouter)

// Default route: single page admin app
app.get("/admin/*", async (req, res) => {
res.send(
renderToHtmlPage(
<IndexPage
username={res.locals.user.fullName}
isSuperuser={res.locals.user.isSuperuser}
gitCmsBranchName={this.gitCmsBranchName}
/>
)
)
})

// Public preview of a Gdoc document
app.get("/gdocs/:id/preview", async (req, res) => {
try {
// TODO: this transaction is only RW because somewhere inside it we fetch images
await db.knexReadWriteTransaction(async (knex) => {
const gdoc = await getAndLoadGdocById(
knex,
req.params.id,
GdocsContentSource.Gdocs
)

res.set("X-Robots-Tag", "noindex")
res.send(
renderToHtmlPage(
<OwidGdocPage
baseUrl={BAKED_BASE_URL}
gdoc={gdoc}
debug
isPreviewing
/>
)
)
})
} catch (error) {
console.error("Error fetching gdoc preview", error)
res.status(500).json({
error: { message: String(error), status: 500 },
})
}
})

// From the docs: "this handles any errors that Express catches. This
// needs to go before other error handlers. BugSnag will call the `next`
// error handler if it exists.
if (bugsnagMiddleware) app.use(bugsnagMiddleware.errorHandler)

// todo: we probably always want to have this, and can remove the isDev
if (this.options.isDev) app.use("/", mockSiteRouter)

// Give full error messages, including in production
app.use(this.errorHandler)

await this.connectToDatabases()

this.server = await this.listenPromise(
app,
adminServerPort,
adminServerHost
)
this.server.timeout = 5 * 60 * 1000 // Increase server timeout for long-running uploads

if (!this.options.quiet)
console.log(
`owid-admin server started on http://${adminServerHost}:${adminServerPort}`
)
}

// Server.listen does not seem to have an async/await form yet.
// https://github.com/expressjs/express/pull/3675
// https://github.com/nodejs/node/issues/21482
private listenPromise(
app: express.Express,
adminServerPort: number,
adminServerHost: string
): Promise<http.Server> {
return new Promise((resolve) => {
const server = app.listen(adminServerPort, adminServerHost, () => {
resolve(server)
})
})
}

errorHandler = async (
err: any,
req: express.Request,
res: express.Response,
// keep `next` because Express only passes errors to handlers with 4 parameters.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
next: NextFunction
) => {
if (!res.headersSent) {
res.status(err.status || 500)
res.send({
error: {
message: err.stack || err,
status: err.status || 500,
},
})
} else {
res.write(
JSON.stringify({
error: {
message: err.stack || err,
status: err.status || 500,
},
})
)
res.end()
}
}

connectToDatabases = async () => {
try {
const _ = db.knexInstance()
} catch (error) {
// grapher database is in fact required, but we will not fail now in case it
// comes online later
if (!this.options.quiet) {
console.error(error)
console.warn(
"Could not connect to grapher database. Continuing without DB..."
)
}
}

if (wpdb.isWordpressDBEnabled) {
try {
await wpdb.singleton.connect()
} catch (error) {
if (!this.options.quiet) {
console.error(error)
console.warn(
"Could not connect to Wordpress database. Continuing without Wordpress..."
)
}
}
} else if (!this.options.quiet) {
console.log(
"WORDPRESS_DB_NAME is not configured -- continuing without Wordpress DB"
)
}

if (!wpdb.isWordpressAPIEnabled && !this.options.quiet) {
console.log(
"WORDPRESS_API_URL is not configured -- continuing without Wordpress API"
)
}
}
}
import { OwidAdminApp } from "./appClass.js"

if (!module.parent)
void new OwidAdminApp({
Expand Down
Loading

0 comments on commit 506aa61

Please sign in to comment.