Add PM2 deployment support
This commit is contained in:
@@ -20,6 +20,36 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('AuthService', () => {
|
||||
it('creates the initial administrator only when the database is empty', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'product-issue-auth-'))
|
||||
directories.push(directory)
|
||||
const database = openDatabase(join(directory, 'database.sqlite'))
|
||||
databases.push(database)
|
||||
const auth = new AuthService(database, 'secret', 60 * 60 * 1_000)
|
||||
|
||||
await expect(
|
||||
auth.createInitialAdmin({
|
||||
email: 'first@example.com',
|
||||
displayName: 'First admin',
|
||||
password: 'first secure password'
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
await expect(
|
||||
auth.createInitialAdmin({
|
||||
email: 'second@example.com',
|
||||
displayName: 'Second admin',
|
||||
password: 'second secure password'
|
||||
})
|
||||
).resolves.toBe(false)
|
||||
|
||||
await expect(
|
||||
auth.login('first@example.com', 'first secure password')
|
||||
).resolves.toMatchObject({ displayName: 'First admin' })
|
||||
await expect(
|
||||
auth.login('second@example.com', 'second secure password')
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('invalidates existing sessions when an administrator is reset', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'product-issue-auth-'))
|
||||
directories.push(directory)
|
||||
|
||||
+49
-4
@@ -1,7 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { loadConfig } from '../src/server/config.js'
|
||||
|
||||
const secret = 'test-secret-that-is-at-least-thirty-two-characters'
|
||||
const directories: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of directories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('loadConfig', () => {
|
||||
it('normalizes a valid production origin', () => {
|
||||
@@ -40,11 +50,46 @@ describe('loadConfig', () => {
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('rejects the example placeholder secret', () => {
|
||||
it('accepts an explicitly configured secret without a length restriction', () => {
|
||||
expect(loadConfig({ APP_SECRET: 'short' }).appSecret).toBe('short')
|
||||
})
|
||||
|
||||
it('generates and reuses a secret when APP_SECRET is omitted', () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'product-issue-config-'))
|
||||
directories.push(directory)
|
||||
|
||||
const first = loadConfig({ DATA_DIRECTORY: directory })
|
||||
const second = loadConfig({ DATA_DIRECTORY: directory })
|
||||
|
||||
expect(first.appSecret).toHaveLength(43)
|
||||
expect(second.appSecret).toBe(first.appSecret)
|
||||
expect(readFileSync(join(directory, '.app-secret'), 'utf8').trim()).toBe(
|
||||
first.appSecret
|
||||
)
|
||||
})
|
||||
|
||||
it('parses initial administrator configuration', () => {
|
||||
expect(
|
||||
loadConfig({
|
||||
APP_SECRET: secret,
|
||||
INITIAL_ADMIN_EMAIL: 'admin@example.com',
|
||||
INITIAL_ADMIN_PASSWORD: 'a local admin password'
|
||||
}).initialAdmin
|
||||
).toEqual({
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Administrator',
|
||||
password: 'a local admin password'
|
||||
})
|
||||
})
|
||||
|
||||
it('requires initial administrator email and password together', () => {
|
||||
expect(() =>
|
||||
loadConfig({
|
||||
APP_SECRET: 'replace-with-at-least-32-random-characters'
|
||||
APP_SECRET: secret,
|
||||
INITIAL_ADMIN_EMAIL: 'admin@example.com'
|
||||
})
|
||||
).toThrow()
|
||||
).toThrow(
|
||||
'INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD must be configured together'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { loadEnvironmentFile } from '../src/server/environment.js'
|
||||
|
||||
const testVariable = 'PRODUCT_ISSUE_ENV_FILE_TEST'
|
||||
const originalValue = process.env[testVariable]
|
||||
const temporaryDirectories: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
if (originalValue === undefined) {
|
||||
delete process.env[testVariable]
|
||||
} else {
|
||||
process.env[testVariable] = originalValue
|
||||
}
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function environmentFile(value: string): string {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'product-issue-env-'))
|
||||
temporaryDirectories.push(directory)
|
||||
const filePath = join(directory, '.env')
|
||||
writeFileSync(filePath, `${testVariable}=${value}\n`, {
|
||||
encoding: 'utf8',
|
||||
mode: 0o600
|
||||
})
|
||||
return filePath
|
||||
}
|
||||
|
||||
describe('loadEnvironmentFile', () => {
|
||||
it('loads values from an explicit environment file', () => {
|
||||
delete process.env[testVariable]
|
||||
|
||||
const filePath = environmentFile('from-file')
|
||||
|
||||
expect(loadEnvironmentFile(filePath)).toBe(filePath)
|
||||
expect(process.env[testVariable]).toBe('from-file')
|
||||
})
|
||||
|
||||
it('does not override values already provided by the process', () => {
|
||||
process.env[testVariable] = 'from-process'
|
||||
|
||||
loadEnvironmentFile(environmentFile('from-file'))
|
||||
|
||||
expect(process.env[testVariable]).toBe('from-process')
|
||||
})
|
||||
|
||||
it('fails when an explicitly configured file does not exist', () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'product-issue-env-'))
|
||||
temporaryDirectories.push(directory)
|
||||
|
||||
expect(() => loadEnvironmentFile(join(directory, '.env'))).toThrow(
|
||||
'Environment file does not exist'
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user