• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cameri / nostream / 25601018106

09 May 2026 12:22PM UTC coverage: 33.99% (-31.1%) from 65.107%
25601018106

Pull #615

github

web-flow
Merge 1ef509ec3 into 36e5af87e
Pull Request #615: test: add unit tests for remaining app workers (#489)

788 of 3170 branches covered (24.86%)

Branch coverage included in aggregate %.

0 of 8 new or added lines in 2 files covered. (0.0%)

1822 existing lines in 87 files now uncovered.

2352 of 6068 relevant lines covered (38.76%)

13.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/cli/utils/config.ts
UNCOV
1
import fs from 'fs'
×
UNCOV
2
import yaml from 'js-yaml'
×
UNCOV
3
import { mergeDeepRight } from 'ramda'
×
4

5
import { Settings } from '../../@types/settings'
UNCOV
6
import { getConfigBaseDir, getDefaultSettingsFilePath, getSettingsFilePath } from './paths'
×
7

8
export type ValidationIssue = {
9
  path: string
10
  message: string
11
}
12

13
type PathToken =
14
  | {
15
      type: 'key'
16
      key: string
17
    }
18
  | {
19
      type: 'index'
20
      index: number
21
    }
22

UNCOV
23
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
×
UNCOV
24
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
×
25
}
26

UNCOV
27
const parsePath = (path: string): PathToken[] => {
×
UNCOV
28
  const input = path.trim()
×
29

UNCOV
30
  if (!input) {
×
31
    throw new Error('Path is required')
×
32
  }
33

UNCOV
34
  const tokens: PathToken[] = []
×
UNCOV
35
  const segments = input.split('.').map((part) => part.trim())
×
36

UNCOV
37
  for (const segment of segments) {
×
UNCOV
38
    if (!segment) {
×
39
      throw new Error(`Invalid path segment in: ${path}`)
×
40
    }
41

UNCOV
42
    const match = segment.match(/^([A-Za-z_][A-Za-z0-9_]*)(\[(\d+)\])*$/)
×
UNCOV
43
    if (!match) {
×
UNCOV
44
      throw new Error(`Invalid path segment: ${segment}`)
×
45
    }
46

UNCOV
47
    tokens.push({ type: 'key', key: match[1] })
×
48

UNCOV
49
    const indexes = [...segment.matchAll(/\[(\d+)\]/g)]
×
UNCOV
50
    for (const entry of indexes) {
×
UNCOV
51
      tokens.push({
×
52
        type: 'index',
53
        index: Number(entry[1]),
54
      })
55
    }
56
  }
57

UNCOV
58
  return tokens
×
59
}
60

UNCOV
61
const formatPathTokens = (tokens: PathToken[]): string => {
×
UNCOV
62
  let out = ''
×
63

UNCOV
64
  for (const token of tokens) {
×
65
    if (token.type === 'key') {
×
66
      out = out ? `${out}.${token.key}` : token.key
×
67
      continue
×
68
    }
69

70
    out = `${out}[${token.index}]`
×
71
  }
72

UNCOV
73
  return out
×
74
}
75

UNCOV
76
export const parseValue = (raw: string): unknown => {
×
UNCOV
77
  const trimmed = raw.trim()
×
78

UNCOV
79
  if (trimmed === 'true') {
×
UNCOV
80
    return true
×
81
  }
82

UNCOV
83
  if (trimmed === 'false') {
×
UNCOV
84
    return false
×
85
  }
86

UNCOV
87
  if (trimmed === 'null') {
×
UNCOV
88
    return null
×
89
  }
90

UNCOV
91
  if (/^-?\d+$/.test(trimmed)) {
×
UNCOV
92
    const asNumber = Number(trimmed)
×
UNCOV
93
    if (Number.isSafeInteger(asNumber)) {
×
UNCOV
94
      return asNumber
×
95
    }
96
  }
97

UNCOV
98
  if (/^-?\d+n$/.test(trimmed)) {
×
UNCOV
99
    return BigInt(trimmed.slice(0, -1))
×
100
  }
101

UNCOV
102
  if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
×
103
    try {
×
104
      return JSON.parse(trimmed)
×
105
    } catch {
106
      return raw
×
107
    }
108
  }
109

UNCOV
110
  return raw
×
111
}
112

UNCOV
113
export const parseTypedValue = (raw: string, type: 'inferred' | 'json' = 'inferred'): unknown => {
×
UNCOV
114
  if (type === 'json') {
×
UNCOV
115
    try {
×
UNCOV
116
      return JSON.parse(raw)
×
117
    } catch (error) {
UNCOV
118
      const message = error instanceof Error ? error.message : String(error)
×
UNCOV
119
      throw new Error(`Invalid JSON value: ${message}`)
×
120
    }
121
  }
122

123
  return parseValue(raw)
×
124
}
125

UNCOV
126
const toSerializable = (value: unknown): unknown => {
×
127
  if (typeof value === 'bigint') {
×
128
    return value.toString()
×
129
  }
130

131
  if (Array.isArray(value)) {
×
132
    return value.map((entry) => toSerializable(entry))
×
133
  }
134

135
  if (isPlainObject(value)) {
×
136
    return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, toSerializable(entry)]))
×
137
  }
138

139
  return value
×
140
}
141

UNCOV
142
const validateShape = (schema: unknown, candidate: unknown, path: PathToken[], issues: ValidationIssue[]): void => {
×
UNCOV
143
  if (schema === undefined || candidate === undefined) {
×
UNCOV
144
    return
×
145
  }
146

UNCOV
147
  const renderedPath = formatPathTokens(path) || '$'
×
148

UNCOV
149
  if (Array.isArray(schema)) {
×
150
    if (!Array.isArray(candidate)) {
×
151
      issues.push({
×
152
        path: renderedPath,
153
        message: `Expected array, got ${typeof candidate}`,
154
      })
155
      return
×
156
    }
157

158
    if (schema.length === 0) {
×
159
      return
×
160
    }
161

162
    candidate.forEach((entry, index) => {
×
163
      const matchesAny = schema.some((schemaEntry) => {
×
164
        const localIssues: ValidationIssue[] = []
×
165
        validateShape(schemaEntry, entry, [...path, { type: 'index', index }], localIssues)
×
166
        return localIssues.length === 0
×
167
      })
168

169
      if (!matchesAny) {
×
170
        issues.push({
×
171
          path: formatPathTokens([...path, { type: 'index', index }]),
172
          message: 'Array element does not match expected schema shape',
173
        })
174
      }
175
    })
176
    return
×
177
  }
178

UNCOV
179
  if (isPlainObject(schema)) {
×
UNCOV
180
    if (!isPlainObject(candidate)) {
×
181
      issues.push({
×
182
        path: renderedPath,
183
        message: `Expected object, got ${typeof candidate}`,
184
      })
185
      return
×
186
    }
187

UNCOV
188
    for (const key of Object.keys(candidate)) {
×
189
      if (!(key in schema)) {
×
190
        issues.push({
×
191
          path: formatPathTokens([...path, { type: 'key', key }]),
192
          message: 'Unknown setting key',
193
        })
194
      }
195
    }
196

UNCOV
197
    for (const key of Object.keys(schema)) {
×
UNCOV
198
      validateShape((schema as Record<string, unknown>)[key], (candidate as Record<string, unknown>)[key], [...path, { type: 'key', key }], issues)
×
199
    }
200

UNCOV
201
    return
×
202
  }
203

204
  if (candidate === null && schema !== null) {
×
205
    issues.push({
×
206
      path: renderedPath,
207
      message: `Expected ${typeof schema}, got null`,
208
    })
209
    return
×
210
  }
211

212
  if (schema !== null && typeof schema !== typeof candidate) {
×
213
    issues.push({
×
214
      path: renderedPath,
215
      message: `Expected ${typeof schema}, got ${typeof candidate}`,
216
    })
217
  }
218
}
219

UNCOV
220
const pathExistsInSchema = (schema: unknown, tokens: PathToken[]): boolean => {
×
UNCOV
221
  let current: unknown = schema
×
222

UNCOV
223
  for (const token of tokens) {
×
UNCOV
224
    if (token.type === 'key') {
×
UNCOV
225
      if (!isPlainObject(current) || !(token.key in current)) {
×
UNCOV
226
        return false
×
227
      }
UNCOV
228
      current = (current as Record<string, unknown>)[token.key]
×
UNCOV
229
      continue
×
230
    }
231

UNCOV
232
    if (!Array.isArray(current)) {
×
233
      return false
×
234
    }
235

UNCOV
236
    current = current[0]
×
237
  }
238

UNCOV
239
  return true
×
240
}
241

UNCOV
242
export const ensureSettingsExists = (): void => {
×
UNCOV
243
  const configDir = getConfigBaseDir()
×
UNCOV
244
  const settingsPath = getSettingsFilePath()
×
UNCOV
245
  const defaultsPath = getDefaultSettingsFilePath()
×
246

UNCOV
247
  if (!fs.existsSync(configDir)) {
×
248
    fs.mkdirSync(configDir, { recursive: true })
×
249
  }
250

UNCOV
251
  if (!fs.existsSync(settingsPath)) {
×
252
    fs.copyFileSync(defaultsPath, settingsPath)
×
253
  }
254
}
255

UNCOV
256
export const loadDefaults = (): Settings => {
×
UNCOV
257
  const defaultsRaw = fs.readFileSync(getDefaultSettingsFilePath(), 'utf-8')
×
UNCOV
258
  return yaml.load(defaultsRaw) as Settings
×
259
}
260

UNCOV
261
export const loadUserSettings = (): Settings => {
×
UNCOV
262
  ensureSettingsExists()
×
UNCOV
263
  const raw = fs.readFileSync(getSettingsFilePath(), 'utf-8')
×
UNCOV
264
  return (yaml.load(raw) as Settings) ?? ({} as Settings)
×
265
}
266

UNCOV
267
export const loadMergedSettings = (): Settings => {
×
UNCOV
268
  return mergeDeepRight(loadDefaults(), loadUserSettings()) as Settings
×
269
}
270

UNCOV
271
export const saveSettings = (settings: Settings): void => {
×
272
  ensureSettingsExists()
×
273
  const serialized = yaml.dump(toSerializable(settings), { lineWidth: 120 })
×
274
  fs.writeFileSync(getSettingsFilePath(), serialized, 'utf-8')
×
275
}
276

UNCOV
277
export const getByPath = (settings: unknown, path: string): unknown => {
×
UNCOV
278
  const tokens = parsePath(path)
×
UNCOV
279
  let current: unknown = settings
×
280

UNCOV
281
  for (const token of tokens) {
×
UNCOV
282
    if (token.type === 'key') {
×
UNCOV
283
      if (!isPlainObject(current)) {
×
284
        return undefined
×
285
      }
UNCOV
286
      current = current[token.key]
×
UNCOV
287
      continue
×
288
    }
289

UNCOV
290
    if (!Array.isArray(current)) {
×
291
      return undefined
×
292
    }
293

UNCOV
294
    current = current[token.index]
×
295
  }
296

UNCOV
297
  return current
×
298
}
299

UNCOV
300
const ensureArrayLength = (target: unknown[], minimumLength: number): void => {
×
UNCOV
301
  while (target.length <= minimumLength) {
×
302
    target.push(undefined)
×
303
  }
304
}
305

UNCOV
306
export const setByPath = (settings: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> => {
×
UNCOV
307
  const tokens = parsePath(path)
×
UNCOV
308
  const clone: Record<string, unknown> = structuredClone(settings)
×
309

UNCOV
310
  if (tokens.length === 0) {
×
311
    throw new Error('Path is required')
×
312
  }
313

UNCOV
314
  let current: unknown = clone
×
315

UNCOV
316
  for (let i = 0; i < tokens.length - 1; i++) {
×
UNCOV
317
    const token = tokens[i]
×
UNCOV
318
    const nextToken = tokens[i + 1]
×
319

UNCOV
320
    if (token.type === 'key') {
×
UNCOV
321
      if (!isPlainObject(current)) {
×
322
        throw new Error(`Cannot set key ${token.key} on non-object path`) 
×
323
      }
324

UNCOV
325
      const existing = current[token.key]
×
UNCOV
326
      if (existing === undefined) {
×
327
        current[token.key] = nextToken.type === 'index' ? [] : {}
×
UNCOV
328
      } else if (nextToken.type === 'index' && !Array.isArray(existing)) {
×
329
        current[token.key] = []
×
UNCOV
330
      } else if (nextToken.type === 'key' && !isPlainObject(existing)) {
×
331
        current[token.key] = {}
×
332
      }
333

UNCOV
334
      current = current[token.key]
×
UNCOV
335
      continue
×
336
    }
337

UNCOV
338
    if (!Array.isArray(current)) {
×
339
      throw new Error(`Cannot index non-array path at [${token.index}]`)
×
340
    }
341

UNCOV
342
    ensureArrayLength(current, token.index)
×
343

UNCOV
344
    const existing = current[token.index]
×
UNCOV
345
    if (existing === undefined) {
×
346
      current[token.index] = nextToken.type === 'index' ? [] : {}
×
UNCOV
347
    } else if (nextToken.type === 'index' && !Array.isArray(existing)) {
×
348
      current[token.index] = []
×
UNCOV
349
    } else if (nextToken.type === 'key' && !isPlainObject(existing)) {
×
350
      current[token.index] = {}
×
351
    }
352

UNCOV
353
    current = current[token.index]
×
354
  }
355

UNCOV
356
  const last = tokens[tokens.length - 1]
×
357

UNCOV
358
  if (last.type === 'key') {
×
UNCOV
359
    if (!isPlainObject(current)) {
×
360
      throw new Error(`Cannot set key ${last.key} on non-object path`)
×
361
    }
362

UNCOV
363
    current[last.key] = value
×
UNCOV
364
    return clone
×
365
  }
366

367
  if (!Array.isArray(current)) {
×
368
    throw new Error(`Cannot index non-array path at [${last.index}]`)
×
369
  }
370

371
  ensureArrayLength(current, last.index)
×
372
  current[last.index] = value
×
373

374
  return clone
×
375
}
376

UNCOV
377
export const validatePathAgainstDefaults = (path: string): ValidationIssue[] => {
×
UNCOV
378
  const defaults = loadDefaults() as unknown
×
UNCOV
379
  const tokens = parsePath(path)
×
380

UNCOV
381
  if (pathExistsInSchema(defaults, tokens)) {
×
UNCOV
382
    return []
×
383
  }
384

UNCOV
385
  return [
×
386
    {
387
      path,
388
      message: 'Path does not exist in default settings schema',
389
    },
390
  ]
391
}
392

UNCOV
393
export const validateSettings = (settings: Settings): ValidationIssue[] => {
×
UNCOV
394
  const issues: ValidationIssue[] = []
×
395

UNCOV
396
  if (!settings.info?.relay_url) {
×
UNCOV
397
    issues.push({ path: 'info.relay_url', message: 'relay_url is required' })
×
398
  }
399

UNCOV
400
  if (!settings.info?.name) {
×
UNCOV
401
    issues.push({ path: 'info.name', message: 'name is required' })
×
402
  }
403

UNCOV
404
  if (!settings.network) {
×
UNCOV
405
    issues.push({ path: 'network', message: 'network section is required' })
×
406
  }
407

UNCOV
408
  if (settings.payments?.enabled && !settings.payments.processor) {
×
409
    issues.push({ path: 'payments.processor', message: 'processor is required when payments are enabled' })
×
410
  }
411

UNCOV
412
  const strategy = settings.limits?.rateLimiter?.strategy
×
UNCOV
413
  if (strategy && strategy !== 'ewma' && strategy !== 'sliding_window') {
×
414
    issues.push({ path: 'limits.rateLimiter.strategy', message: 'strategy must be ewma or sliding_window' })
×
415
  }
416

UNCOV
417
  validateShape(loadDefaults(), settings, [], issues)
×
418

UNCOV
419
  return issues
×
420
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc