OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / set-password.test.js

80 lines · javascript ref:
// applySetPassword — `hpp-onboard --set-password` must write the account the DAEMON actually reads.
//
// The bug: it wrote cfg.appPasswordHash (top level) on configs whose accounts live in cfg.dids[].
// bootMultiDevice spreads the per-DID entry (bootSwarmDevice({ storage, ...entry }),
// device-node.js:694), so sessions come from entry.appPasswordHash and a top-level field is dead.
// The command reported success and changed nothing. The first test below is the control that pins
// that exact shape, so a regression cannot pass by writing the wrong level again.

import test from 'brittle'
import { applySetPassword } from '../lib/set-password.js'

const multi = () => ({
  swarm: true,
  dids: [
    { did: 'did:plc:aaa', handle: 'a.example', appPasswordHash: 'OLD-A', jwtSecret: 'ja' },
    { did: 'did:plc:bbb', handle: 'b.example', appPasswordHash: 'OLD-B', jwtSecret: 'jb' },
    { did: 'did:plc:feed', handle: 'feed.example' } // a co-owned feed: no hash, no jwtSecret, on purpose
  ]
})

test('CONTROL: the per-DID entry is what changes — never a stray top-level field', (t) => {
  const cfg = multi()
  const r = applySetPassword(cfg, { did: 'did:plc:bbb', hash: 'NEW' })

  t.is(cfg.dids[1].appPasswordHash, 'NEW', 'the NAMED account got the new hash')
  t.is(cfg.dids[0].appPasswordHash, 'OLD-A', 'the other account is untouched')
  t.absent('appPasswordHash' in cfg, 'NO top-level appPasswordHash — that field is never read in multi-DID mode')
  t.is(r.mode, 'multi')
  t.is(r.did, 'did:plc:bbb')
  t.is(r.handle, 'b.example', 'reports the handle, so the operator can see which login changed')
  t.absent(r.warning, 'an account with a jwtSecret warns about nothing')
})

test('refuses to GUESS when several accounts are hosted', (t) => {
  const cfg = multi()
  t.exception(() => applySetPassword(cfg, { hash: 'NEW' }), /MULTI-DID config with 3 accounts/,
    'no --did on a multi-account config is a refusal, not a coin flip')
  t.is(cfg.dids[0].appPasswordHash, 'OLD-A', 'and nothing was written on the way out')

  t.exception(() => applySetPassword(cfg, { did: 'did:plc:nope', hash: 'NEW' }), /not a hosted DID/)
  t.is(cfg.dids[1].appPasswordHash, 'OLD-B', 'an unknown DID writes nothing either')
})

test('a single hosted account is unambiguous, so --did is optional', (t) => {
  const cfg = { dids: [{ did: 'did:plc:only', handle: 'only.example', jwtSecret: 'j' }] }
  const r = applySetPassword(cfg, { hash: 'NEW' })
  t.is(cfg.dids[0].appPasswordHash, 'NEW')
  t.is(r.did, 'did:plc:only')
})

test('a feed DID (no jwtSecret) is warned about, not silently accepted', (t) => {
  const cfg = multi()
  const r = applySetPassword(cfg, { did: 'did:plc:feed', hash: 'NEW' })
  t.is(cfg.dids[2].appPasswordHash, 'NEW', 'still written — a two-step setup is legitimate')
  t.ok(r.warning && /jwtSecret/.test(r.warning), 'but it says the password alone will not enable login')
})

test('legacy single-DID config still writes the top level — that IS the account there', (t) => {
  const cfg = { did: 'did:plc:legacy', handle: 'l.example', jwtSecret: 'j', appPasswordHash: 'OLD' }
  const r = applySetPassword(cfg, { hash: 'NEW' })
  t.is(cfg.appPasswordHash, 'NEW')
  t.is(r.mode, 'single')
  t.absent(cfg.dids, 'no dids[] was invented')

  t.exception(() => applySetPassword(cfg, { did: 'did:plc:other', hash: 'X' }), /does not match/,
    'a --did naming a different account is a refusal, not a silent overwrite')
})

test('empty dids[] falls through to the legacy path rather than throwing', (t) => {
  const cfg = { did: 'did:plc:legacy', dids: [], jwtSecret: 'j' }
  const r = applySetPassword(cfg, { hash: 'NEW' })
  t.is(cfg.appPasswordHash, 'NEW')
  t.is(r.mode, 'single')
})

test('bad input is refused', (t) => {
  t.exception(() => applySetPassword(null, { hash: 'X' }), /config object required/)
  t.exception(() => applySetPassword({}, {}), /hash required/)
})