Files
web-terminal/control-plane/test/env.test.ts
Yaojia Wang fff011bb7f feat(control-plane): POST /auth/login mints device:enroll bearer (B1)
Unblocks the phone-enrollment track: an operator-password login mints a
short-lived device:enroll capability token that POST /device/enroll requires.
Constant-time (SHA-256 fixed-length) compare, per-client rate-limit, fail-closed
when unset. 260 tests pass.
2026-07-18 13:32:05 +02:00

118 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, test, expect } from 'vitest'
import { loadEnv, DEFAULT_PAIRING_TTL_SEC, DEFAULT_PAIRING_MAX_REDEEM_ATTEMPTS } from '../src/env.js'
import { bytesToBase64 } from '../src/util/bytes.js'
const validPubkey = bytesToBase64(new Uint8Array(32).fill(7))
const base = (): NodeJS.ProcessEnv => ({
PG_URL: 'postgres://u:p@localhost:5432/cp',
REDIS_URL: 'redis://localhost:6379',
CAPABILITY_SIGN_PUBKEY_B64: validPubkey,
CA_INTERMEDIATE_KMS_KEY_REF: 'kms://key/intermediate',
CA_INTERMEDIATE_CERT_PATH: '/etc/cp/intermediate.pem',
NODE_MTLS_TRUST_BUNDLE_PATH: '/etc/cp/node-ca.pem',
BASE_DOMAIN: 'term.example.com',
})
describe('T1 loadEnv (INV9 fail-fast)', () => {
test('empty env throws listing every missing required key', () => {
let msg = ''
try {
loadEnv({})
} catch (e) {
msg = e instanceof Error ? e.message : ''
}
expect(msg).toContain('PG_URL')
expect(msg).toContain('REDIS_URL')
expect(msg).toContain('CAPABILITY_SIGN_PUBKEY_B64')
expect(msg).toContain('CA_INTERMEDIATE_KMS_KEY_REF')
expect(msg).toContain('BASE_DOMAIN')
})
test('bad pgUrl throws', () => {
expect(() => loadEnv({ ...base(), PG_URL: 'not a url' })).toThrow(/PG_URL/)
})
test('valid map parses', () => {
const env = loadEnv(base())
expect(env.baseDomain).toBe('term.example.com')
expect(env.capabilitySignPubkey.length).toBe(32)
})
test('pairing ttl / max-attempts default when unset', () => {
const env = loadEnv(base())
expect(env.pairingTtlSec).toBe(DEFAULT_PAIRING_TTL_SEC)
expect(env.pairingMaxRedeemAttempts).toBe(DEFAULT_PAIRING_MAX_REDEEM_ATTEMPTS)
})
test('explicit pairing overrides parse', () => {
const env = loadEnv({ ...base(), PAIRING_TTL_SEC: '900', PAIRING_MAX_REDEEM_ATTEMPTS: '3' })
expect(env.pairingTtlSec).toBe(900)
expect(env.pairingMaxRedeemAttempts).toBe(3)
})
test('never echoes secret VALUES on failure (INV9)', () => {
const secret = bytesToBase64(new Uint8Array(32).fill(9))
try {
loadEnv({ ...base(), CAPABILITY_SIGN_PUBKEY_B64: secret, PG_URL: 'bad' })
} catch (e) {
const msg = e instanceof Error ? e.message : ''
expect(msg).not.toContain(secret)
}
})
test('capability pubkey wrong length rejected', () => {
const short = bytesToBase64(new Uint8Array(16))
expect(() => loadEnv({ ...base(), CAPABILITY_SIGN_PUBKEY_B64: short })).toThrow(/32 bytes/)
})
})
describe('B1 operator-login env (set-together-or-none, fail-closed)', () => {
const OPERATOR_PASSWORD = 'operator-secret-abcdef123456'
const ACCOUNT = '11111111-1111-4111-8111-111111111111'
const PRIVKEY = bytesToBase64(new Uint8Array(48).fill(3)) // shape-valid base64; import validated at boot
test('none set → login fields undefined (feature off, route fail-closed at runtime)', () => {
const env = loadEnv(base())
expect(env.operatorPassword).toBeUndefined()
expect(env.operatorAccountId).toBeUndefined()
expect(env.capabilitySignPrivkey).toBeUndefined()
})
test('all three set → parsed together', () => {
const env = loadEnv({
...base(),
OPERATOR_PASSWORD,
OPERATOR_ACCOUNT_ID: ACCOUNT,
CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY,
})
expect(env.operatorPassword).toBe(OPERATOR_PASSWORD)
expect(env.operatorAccountId).toBe(ACCOUNT)
expect(env.capabilitySignPrivkey?.length).toBeGreaterThan(0)
})
test('password without account/key → fail-fast (no silent half-open)', () => {
expect(() => loadEnv({ ...base(), OPERATOR_PASSWORD })).toThrow(/OPERATOR_ACCOUNT_ID|CAPABILITY_SIGN_PRIVKEY_B64/)
})
test('too-short operator password rejected (16512 charset rule)', () => {
expect(() =>
loadEnv({ ...base(), OPERATOR_PASSWORD: 'short', OPERATOR_ACCOUNT_ID: ACCOUNT, CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY }),
).toThrow(/OPERATOR_PASSWORD/)
})
test('non-UUID operator account rejected', () => {
expect(() =>
loadEnv({ ...base(), OPERATOR_PASSWORD, OPERATOR_ACCOUNT_ID: 'not-a-uuid', CAPABILITY_SIGN_PRIVKEY_B64: PRIVKEY }),
).toThrow(/OPERATOR_ACCOUNT_ID/)
})
test('never echoes the operator secret on a partial-config failure (INV9)', () => {
try {
loadEnv({ ...base(), OPERATOR_PASSWORD })
} catch (e) {
expect(e instanceof Error ? e.message : '').not.toContain(OPERATOR_PASSWORD)
}
})
})