Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/utils/isAppRunning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import internalApiRequest from './internalApiRequest.js'

/**
* Checks whether an app instance is already listening on the configured host/port.
*
* Any HTTP response means something is serving, including an error status: the probe requests
* the API endpoint map, which requires authentication, so an unauthenticated probe is answered
* with a 401 rather than a 200. Only a refused connection means nothing is listening.
*
* @param {Object} options Options to pass to the request
* @return {Promise<Boolean>} Whether an app instance is already running
*/
export default async function isAppRunning (options = {}) {
try {
await internalApiRequest('', undefined, { ...options, method: 'GET' })
} catch (e) {
if (e.statusCode !== undefined) return true
if ((e.code ?? e.cause?.code) === 'ECONNREFUSED') return false
throw e
}
return true
}
6 changes: 2 additions & 4 deletions lib/utils/registerSuperUser.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { createInterface } from 'readline'
import prompts from 'prompts'
import internalApiRequest from './internalApiRequest.js'
import isAppRunning from './isAppRunning.js'
import startApp from './startApp.js'

export default async function registerSuperUser (options = {}) {
try {
await internalApiRequest('', undefined, { ...options, method: 'GET' })
} catch (e) {
if ((e.code ?? e.cause?.code) !== 'ECONNREFUSED') throw e
if (!await isAppRunning(options)) {
await startApp(options)
}
let email = options.superEmail
Expand Down
62 changes: 62 additions & 0 deletions tests/isAppRunning.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, it, mock } from 'node:test'
import assert from 'node:assert/strict'

let requestError

// mock internalApiRequest before importing isAppRunning
mock.module('../lib/utils/internalApiRequest.js', {
defaultExport: async () => {
if (requestError) throw requestError
}
})

const { default: isAppRunning } = await import('../lib/utils/isAppRunning.js')

/** Builds the error internalApiRequest throws for a non-2xx response */
function httpError (code, statusCode) {
const e = new Error(code)
e.code = code
e.statusCode = statusCode
return e
}

/** Builds the error fetch throws when it can't connect */
function transportError (code) {
const e = new TypeError('fetch failed')
e.cause = Object.assign(new Error(code), { code })
return e
}

describe('isAppRunning', () => {
it('should return true when the request succeeds', async () => {
requestError = undefined
assert.equal(await isAppRunning(), true)
})

it('should return true when the app responds with an auth error', async () => {
requestError = httpError('MISSING_AUTH_HEADER', 401)
assert.equal(await isAppRunning(), true)
})

it('should return true for any other error status', async () => {
for (const [code, status] of [['UNAUTHORISED', 403], ['NOT_FOUND', 404], ['SERVER_ERROR', 500]]) {
requestError = httpError(code, status)
assert.equal(await isAppRunning(), true)
}
})

it('should return false when the connection is refused', async () => {
requestError = transportError('ECONNREFUSED')
assert.equal(await isAppRunning(), false)
})

it('should rethrow errors which are neither a response nor a refused connection', async () => {
requestError = new Error('Cannot find module conf/production.config.js')
await assert.rejects(() => isAppRunning(), /Cannot find module/)
})

it('should rethrow other transport errors', async () => {
requestError = transportError('EHOSTUNREACH')
await assert.rejects(() => isAppRunning(), e => e.cause.code === 'EHOSTUNREACH')
})
})
Loading