@dooer/dooer-test-env 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -17,6 +17,7 @@ module.exports = async function cli() {
17
17
  .command(require('./command/customer')) // customer copy|purge
18
18
  .command(require('./command/shred')) // shred (localhost only)
19
19
  .command(require('./command/bankid')) // bankid pull|status|clear (staging certs → keychain)
20
+ .command(require('./command/validation')) // validation on|off|status (output-schema checks)
20
21
  .command(require('./command/logs')) // search logs across all local services (transactionId, errors, …)
21
22
  .command(require('./command/measure')) // resource-usage report for the running env
22
23
  .demandCommand(1, 'Pick a command group. Try --help.')
@@ -14,7 +14,11 @@ const discovery = require('../discovery/client')
14
14
  function ensureCompose() {
15
15
  rt.ensureRunDir()
16
16
  if (!fs.existsSync(rt.COMPOSE_FILE)) {
17
- const r = generateCompose({ servicesDir: rt.SERVICES_DIR, out: rt.COMPOSE_FILE })
17
+ const r = generateCompose({
18
+ servicesDir: rt.SERVICES_DIR,
19
+ out: rt.COMPOSE_FILE,
20
+ outputValidation: !!rt.readState().outputValidation,
21
+ })
18
22
  console.log(chalk.gray(`generated ${rt.COMPOSE_FILE} (${(r && r.serviceCount) || 'n'} services)`))
19
23
  }
20
24
  return rt.COMPOSE_FILE
@@ -62,7 +62,11 @@ module.exports = {
62
62
 
63
63
  // generate the compose file
64
64
  rt.ensureRunDir()
65
- const r = generateCompose({ servicesDir: rt.SERVICES_DIR, out: rt.COMPOSE_FILE })
65
+ const r = generateCompose({
66
+ servicesDir: rt.SERVICES_DIR,
67
+ out: rt.COMPOSE_FILE,
68
+ outputValidation: !!rt.readState().outputValidation,
69
+ })
66
70
  console.log(`\n${ok(true)} generated ${rt.COMPOSE_FILE} (${(r && r.serviceCount) || '?'} services)`)
67
71
 
68
72
  // BankID certs → keychain, so local login matches staging (production BankID). Idempotent; only pulls
@@ -0,0 +1,74 @@
1
+ const chalk = require('chalk')
2
+ const rt = require('../runtime')
3
+ const { generateCompose, VALIDATION_ENV } = require('../compose/generate')
4
+
5
+ // Turn OUTPUT-schema validation on/off for the whole local stack.
6
+ //
7
+ // Both @dooer/microservice (a service validating its OWN response, define-route.js:515) and
8
+ // @dooer/json-http-client (a client validating a response it RECEIVED, route-validators.js:46) run that
9
+ // check only when NODE_ENV !== 'production'. Inbound INPUT validation is a separate path
10
+ // (define-route.js:436) and always runs — this switch does not weaken it.
11
+ //
12
+ // Default OFF, so the local env behaves like staging/live where these checks are silent. Turn it ON to hunt
13
+ // stale service definitions (a consumer whose bundled definition predates a producer's added fields 500s on
14
+ // perfectly good data). See lib/compose/generate.js VALIDATION_ENV.
15
+
16
+ function apply(enabled, { profile }) {
17
+ const state = rt.readState()
18
+ state.outputValidation = !!enabled
19
+ rt.writeState(state)
20
+ const r = generateCompose({ servicesDir: rt.SERVICES_DIR, out: rt.COMPOSE_FILE, outputValidation: !!enabled })
21
+ console.log(chalk.gray(`regenerated ${rt.COMPOSE_FILE} (${r.dooerServiceCount} services)`))
22
+ const code = rt.dc(['up', '-d'], { profile })
23
+ console.log(
24
+ code === 0
25
+ ? chalk.green(`output validation ${enabled ? 'ON' : 'OFF'} — services recreated.`)
26
+ : chalk.yellow('compose reported an error while recreating services')
27
+ )
28
+ process.exitCode = code
29
+ }
30
+
31
+ const profileOption = (y) =>
32
+ y.option('profile', { type: 'string', default: 'full', describe: 'compose profile to recreate' })
33
+
34
+ module.exports = {
35
+ command: 'validation <command>',
36
+ describe: 'output-schema validation for the local stack (default: off, like staging/live)',
37
+ builder: (y) =>
38
+ y
39
+ .command({
40
+ command: 'on',
41
+ describe: 'ENABLE output-schema validation (surfaces stale service definitions; recreates services)',
42
+ builder: profileOption,
43
+ handler: (argv) => apply(true, { profile: argv.profile }),
44
+ })
45
+ .command({
46
+ command: 'off',
47
+ describe: 'DISABLE output-schema validation (default; behaves like staging/live)',
48
+ builder: profileOption,
49
+ handler: (argv) => apply(false, { profile: argv.profile }),
50
+ })
51
+ .command({
52
+ command: 'status',
53
+ describe: 'show whether output-schema validation is on or off',
54
+ handler: () => {
55
+ const on = !!rt.readState().outputValidation
56
+ console.log(`output validation: ${on ? chalk.green('ON') : chalk.gray('OFF')}`)
57
+ console.log(
58
+ ` ${Object.entries(VALIDATION_ENV[on ? 'on' : 'off'])
59
+ .map(([k, v]) => `${k}=${v}`)
60
+ .join(' ')}`
61
+ )
62
+ console.log(
63
+ chalk.gray(
64
+ on
65
+ ? ' a consumer with a stale service definition will 500 on valid data — that is the point'
66
+ : ' matches staging/live, where these checks are silent. `validation on` to hunt drift'
67
+ )
68
+ )
69
+ },
70
+ })
71
+ .demandCommand(1, 'Use: validation on | validation off | validation status')
72
+ .strict(),
73
+ handler: () => {},
74
+ }
@@ -82,8 +82,7 @@ const GLOBAL_OVERRIDES = {
82
82
  // NOTE: never set DOOER_MICROSERVICE_PORT — no staging manifest sets it, so every image defaults to 3000
83
83
  // on its own. service-ai (python) reads it as a raw string and does `bind(('', port))`, which throws
84
84
  // "an integer is required (got type str)" the moment we inject it. Let the images default.
85
- NODE_ENV: 'development',
86
- DOOER_ENVIRONMENT_NAME: 'local',
85
+ // NODE_ENV / DOOER_ENVIRONMENT_NAME are set per validation mode — see VALIDATION_ENV below.
87
86
  DOOER_LOG_LEVEL: 'informational',
88
87
  // Auth: force the shared @dooer LOCAL dev PUBLIC key on every verifier. The manifests carry the STAGING
89
88
  // public key as a plain value (which would win over config's `local` default), but the only signer —
@@ -149,8 +148,26 @@ function secretDefault(name) {
149
148
  return OMIT
150
149
  }
151
150
 
151
+ // OUTPUT-schema validation mode. Both @dooer/microservice (a service validating its OWN response,
152
+ // define-route.js:515) and @dooer/json-http-client (a client validating a response it RECEIVED,
153
+ // route-validators.js:46) run that check only when NODE_ENV !== 'production'. Inbound INPUT validation is
154
+ // a different path (define-route.js:436) and always runs — turning this off does not weaken it.
155
+ //
156
+ // OFF is the default so the local env behaves like staging/live, where these checks are silent: otherwise
157
+ // any consumer carrying a stale service definition 500s on perfectly good data (seen with service-ledger →
158
+ // service-business-entities and service-document-processing → service-core-objects). Turn it ON deliberately
159
+ // to hunt that drift. (Jimmy 2026-09-03.)
160
+ //
161
+ // With NODE_ENV=production, @dooer/config's isDevOrTest() would go false and drop every `local:` value and
162
+ // `localOptional` (crash-looping services) — DOOER_ENVIRONMENT_NAME='environment-local' keeps it true via
163
+ // its second clause. DOOER_DISPLAY_ERRORS keeps error detail, which production mode otherwise hides.
164
+ const VALIDATION_ENV = {
165
+ on: { NODE_ENV: 'development', DOOER_ENVIRONMENT_NAME: 'local', DOOER_DISPLAY_ERRORS: 'true' },
166
+ off: { NODE_ENV: 'production', DOOER_ENVIRONMENT_NAME: 'environment-local', DOOER_DISPLAY_ERRORS: 'true' },
167
+ }
168
+
152
169
  // Build the final env map for one dooer service.
153
- function buildServiceEnv(svc) {
170
+ function buildServiceEnv(svc, { outputValidation = false } = {}) {
154
171
  const env = {}
155
172
  // 1. plain manifest values (verbatim).
156
173
  Object.assign(env, svc.env)
@@ -160,8 +177,8 @@ function buildServiceEnv(svc) {
160
177
  if (v === OMIT) return
161
178
  env[name] = v
162
179
  })
163
- // 3. global local overrides win.
164
- Object.assign(env, GLOBAL_OVERRIDES)
180
+ // 3. global local overrides win, plus the validation-mode env (NODE_ENV / environment name).
181
+ Object.assign(env, GLOBAL_OVERRIDES, VALIDATION_ENV[outputValidation ? 'on' : 'off'])
165
182
  // 3b. stateless services (service-graphql, service-pdf, …) declare no application user; keep the shared
166
183
  // dooer superuser for them so they still connect. Services WITH a manifest user keep it (see GLOBAL_OVERRIDES).
167
184
  if (!env.DOOER_SQL_APPLICATION_USER) env.DOOER_SQL_APPLICATION_USER = DEV.sqlUser
@@ -362,7 +379,7 @@ const SERVICE_ALIASES = {
362
379
  }
363
380
 
364
381
  // Turn the parsed manifests into the compose object.
365
- function buildCompose(services, frontends, { profile } = {}) {
382
+ function buildCompose(services, frontends, { profile, outputValidation = false } = {}) {
366
383
  const wantedSvc = profile ? services.filter((s) => profilesFor(s.name).includes(profile)) : services
367
384
  const portMap = frontendPortMap(frontends)
368
385
 
@@ -376,7 +393,10 @@ function buildCompose(services, frontends, { profile } = {}) {
376
393
  // Fall back to ./CMD-OPF.sh when a manifest sets none, so we never hit the image default ./CMD.sh
377
394
  // (Consul path) which hangs on consul-template.
378
395
  command: svc.command || ['./CMD-OPF.sh'],
379
- environment: rewriteBrowserUrls({ ...buildServiceEnv(svc), ...(SERVICE_ENV_OVERRIDES[svc.name] || {}) }, portMap),
396
+ environment: rewriteBrowserUrls(
397
+ { ...buildServiceEnv(svc, { outputValidation }), ...(SERVICE_ENV_OVERRIDES[svc.name] || {}) },
398
+ portMap
399
+ ),
380
400
  // publish the browser-facing facade (4000) and service-graphql (4001) on the host
381
401
  ...(svc.name === 'public-facade' ? { ports: [`${FACADE_HOST_PORT}:3000`] } : {}),
382
402
  ...(svc.name === 'service-graphql' ? { ports: [`${GRAPHQL_HOST_PORT}:3000`] } : {}),
@@ -394,7 +414,7 @@ function buildCompose(services, frontends, { profile } = {}) {
394
414
  // repointed to each frontend's own localhost port where we run it (others stay as-is).
395
415
  ;(frontends || []).forEach((fe) => {
396
416
  const hostPort = portMap[fe.name]
397
- const env = buildServiceEnv(fe)
417
+ const env = buildServiceEnv(fe, { outputValidation })
398
418
  // dooer-frontend's config validation rejects an EMPTY sentryEndpoint for the `local` environment
399
419
  // ("No value specified for [sentryEndpoint] ... (ENV: 'DOOER_SENTRY_ENDPOINT')"), even though staging
400
420
  // ships it as "". We don't use Sentry (Jimmy 2026-09-01) — inject a well-formed but inert DSN so the
@@ -430,10 +450,10 @@ function buildCompose(services, frontends, { profile } = {}) {
430
450
 
431
451
  // Public entry point. Reads every `service-*.yaml` + `frontend-*.yaml` in `servicesDir`, builds the
432
452
  // compose object, writes it as YAML to `out`, and returns a small summary.
433
- function generateCompose({ profile, servicesDir, out } = {}) {
453
+ function generateCompose({ profile, servicesDir, out, outputValidation = false } = {}) {
434
454
  const services = listServices(servicesDir)
435
455
  const frontends = listFrontends(servicesDir)
436
- const compose = buildCompose(services, frontends, { profile })
456
+ const compose = buildCompose(services, frontends, { profile, outputValidation })
437
457
 
438
458
  const yamlText = yaml.safeDump(compose, {
439
459
  lineWidth: -1, // never wrap (long base64 public keys must stay one line)
@@ -459,6 +479,7 @@ function generateCompose({ profile, servicesDir, out } = {}) {
459
479
 
460
480
  module.exports = {
461
481
  BOOKING_PROFILE_SERVICES,
482
+ VALIDATION_ENV,
462
483
  GLOBAL_OVERRIDES,
463
484
  secretDefault,
464
485
  buildServiceEnv,
@@ -133,7 +133,22 @@ Optional:
133
133
  function sh(file, args) {
134
134
  return execFileSync(file, args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).trim()
135
135
  }
136
+ // The LOCAL test env is addressed as the pseudo-namespace `local`: the docker-compose Postgres published on
137
+ // 55432, and MinIO standing in for Ceph. Lets `customer copy --target-namespace local` pull a real org out
138
+ // of staging/production into the local stack. (Jimmy 2026-09-03.)
139
+ const LOCAL_NAMESPACE = 'local'
140
+ const isLocalNamespace = (namespace) => namespace === LOCAL_NAMESPACE
141
+
136
142
  function connInfo(namespace) {
143
+ if (isLocalNamespace(namespace)) {
144
+ return {
145
+ host: 'localhost',
146
+ port: Number(process.env.DOOER_TEST_ENV_PG_PORT || 55432),
147
+ user: 'dooer',
148
+ password: 'dooer',
149
+ database: 'dooer',
150
+ }
151
+ }
137
152
  const clusterIP = sh('kubectl', ['get', 'svc', 'dooer-database', '-n', namespace, '-o', 'jsonpath={.spec.clusterIP}'])
138
153
  const b64 = sh('kubectl', [
139
154
  'get',
@@ -236,6 +251,21 @@ async function withRetry(fn, { tries = 6, label = 'op' } = {}) {
236
251
  // from the SOURCE env bucket to the TARGET env bucket at its NEW key (= the copied fileData id), so the
237
252
  // copy owns its own files. Endpoint/bucket/credentials are read from the file-storage pod per namespace.
238
253
  function s3Config(namespace) {
254
+ if (isLocalNamespace(namespace)) {
255
+ // local MinIO — the bucket `minio-init` provisions (see lib/compose/generate.js S3_BUCKETS).
256
+ return {
257
+ primary: {
258
+ bucket: 'file-storage-bucket',
259
+ client: new S3Client({
260
+ endpoint: process.env.DOOER_TEST_ENV_S3 || 'http://localhost:9000',
261
+ region: 'us-east-1',
262
+ forcePathStyle: true,
263
+ credentials: { accessKeyId: 'minioadmin', secretAccessKey: 'minioadmin' },
264
+ }),
265
+ },
266
+ fallback: null,
267
+ }
268
+ }
239
269
  const pods = sh('kubectl', [
240
270
  'get',
241
271
  'pods',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dooer/dooer-test-env",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Run the whole Dooer backend locally (staging DB minus customers), copy/purge customers between environments, and shred — one CLI.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": "Dooer/cli-dooer-test-env",
package/readme.md CHANGED
@@ -43,6 +43,7 @@ customer new "<name>" --owner <userId> --execute empty functional account: compa
43
43
  customer copy | customer purge copy / delete one org between environments (emails anonymized)
44
44
  shred anonymize the LOCAL db (localhost only)
45
45
  bankid pull | status | clear BankID cert (staging → keychain), used by service-accounts
46
+ validation on | off | status output-schema validation (default OFF, like staging/live)
46
47
  logs <pattern> [--since 5m] [--service x] [-i] search logs across ALL services (e.g. a transactionId). alias: search
47
48
  measure sample docker stats → resource-usage report
48
49
  service <start|stop|restart|deploy|local|unlocal|version> <name> per-service control