publish(task 2): config-loader mit env-validation
loadConfig() liest 3 pflicht-keys (BUNKER_URL, AUTHOR_PUBKEY_HEX, BOOTSTRAP_RELAY) und 3 optionale mit defaults (CONTENT_ROOT, CLIENT_TAG=leer, MIN_RELAY_ACKS=2). pubkey-hex-format validiert (64 lowercase hex), MIN_RELAY_ACKS als positive integer. 6 tests, alle grün. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6b6502a22c
commit
1e4359aab6
|
|
@ -0,0 +1,47 @@
|
|||
export interface Config {
|
||||
bunkerUrl: string
|
||||
authorPubkeyHex: string
|
||||
bootstrapRelay: string
|
||||
contentRoot: string
|
||||
clientTag: string
|
||||
minRelayAcks: number
|
||||
}
|
||||
|
||||
type EnvReader = (key: string) => string | undefined
|
||||
|
||||
const REQUIRED = ['BUNKER_URL', 'AUTHOR_PUBKEY_HEX', 'BOOTSTRAP_RELAY'] as const
|
||||
|
||||
const DEFAULTS = {
|
||||
CONTENT_ROOT: '../content/posts',
|
||||
CLIENT_TAG: '',
|
||||
MIN_RELAY_ACKS: '2',
|
||||
}
|
||||
|
||||
export function loadConfig(read: EnvReader = (k) => Deno.env.get(k)): Config {
|
||||
const missing: string[] = []
|
||||
const values: Record<string, string> = {}
|
||||
for (const key of REQUIRED) {
|
||||
const v = read(key)
|
||||
if (!v) missing.push(key)
|
||||
else values[key] = v
|
||||
}
|
||||
if (missing.length) {
|
||||
throw new Error(`Missing env: ${missing.join(', ')}`)
|
||||
}
|
||||
if (!/^[0-9a-f]{64}$/.test(values.AUTHOR_PUBKEY_HEX)) {
|
||||
throw new Error('AUTHOR_PUBKEY_HEX must be 64 lowercase hex characters')
|
||||
}
|
||||
const minAcksRaw = read('MIN_RELAY_ACKS') ?? DEFAULTS.MIN_RELAY_ACKS
|
||||
const minAcks = Number(minAcksRaw)
|
||||
if (!Number.isInteger(minAcks) || minAcks < 1) {
|
||||
throw new Error(`MIN_RELAY_ACKS must be a positive integer, got "${minAcksRaw}"`)
|
||||
}
|
||||
return {
|
||||
bunkerUrl: values.BUNKER_URL,
|
||||
authorPubkeyHex: values.AUTHOR_PUBKEY_HEX,
|
||||
bootstrapRelay: values.BOOTSTRAP_RELAY,
|
||||
contentRoot: read('CONTENT_ROOT') ?? DEFAULTS.CONTENT_ROOT,
|
||||
clientTag: read('CLIENT_TAG') ?? DEFAULTS.CLIENT_TAG,
|
||||
minRelayAcks: minAcks,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { assertEquals, assertThrows } from '@std/assert'
|
||||
import { loadConfig } from '../src/core/config.ts'
|
||||
|
||||
const REQUIRED = {
|
||||
BUNKER_URL: 'bunker://abc?relay=wss://r.example&secret=s',
|
||||
AUTHOR_PUBKEY_HEX: '4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41',
|
||||
BOOTSTRAP_RELAY: 'wss://relay.damus.io',
|
||||
}
|
||||
|
||||
Deno.test('loadConfig: liest alle pflicht-keys aus env', () => {
|
||||
const cfg = loadConfig((k) => REQUIRED[k as keyof typeof REQUIRED])
|
||||
assertEquals(cfg.bunkerUrl, REQUIRED.BUNKER_URL)
|
||||
assertEquals(cfg.authorPubkeyHex, REQUIRED.AUTHOR_PUBKEY_HEX)
|
||||
assertEquals(cfg.bootstrapRelay, REQUIRED.BOOTSTRAP_RELAY)
|
||||
})
|
||||
|
||||
Deno.test('loadConfig: liefert defaults für optionale keys', () => {
|
||||
const cfg = loadConfig((k) => REQUIRED[k as keyof typeof REQUIRED])
|
||||
assertEquals(cfg.contentRoot, '../content/posts')
|
||||
assertEquals(cfg.clientTag, '')
|
||||
assertEquals(cfg.minRelayAcks, 2)
|
||||
})
|
||||
|
||||
Deno.test('loadConfig: optionale keys können überschrieben werden', () => {
|
||||
const env = {
|
||||
...REQUIRED,
|
||||
CONTENT_ROOT: '../blog',
|
||||
CLIENT_TAG: 'my-site',
|
||||
MIN_RELAY_ACKS: '3',
|
||||
}
|
||||
const cfg = loadConfig((k) => env[k as keyof typeof env])
|
||||
assertEquals(cfg.contentRoot, '../blog')
|
||||
assertEquals(cfg.clientTag, 'my-site')
|
||||
assertEquals(cfg.minRelayAcks, 3)
|
||||
})
|
||||
|
||||
Deno.test('loadConfig: wirft bei fehlender pflicht-variable', () => {
|
||||
assertThrows(() => loadConfig(() => undefined), Error, 'BUNKER_URL')
|
||||
})
|
||||
|
||||
Deno.test('loadConfig: validiert pubkey-format (64 hex)', () => {
|
||||
const env = { ...REQUIRED, AUTHOR_PUBKEY_HEX: 'zzz' }
|
||||
assertThrows(
|
||||
() => loadConfig((k) => env[k as keyof typeof env]),
|
||||
Error,
|
||||
'AUTHOR_PUBKEY_HEX',
|
||||
)
|
||||
})
|
||||
|
||||
Deno.test('loadConfig: MIN_RELAY_ACKS muss positiv sein', () => {
|
||||
const env = { ...REQUIRED, MIN_RELAY_ACKS: '0' }
|
||||
assertThrows(
|
||||
() => loadConfig((k) => env[k as keyof typeof env]),
|
||||
Error,
|
||||
'MIN_RELAY_ACKS',
|
||||
)
|
||||
})
|
||||
Loading…
Reference in New Issue