test: close the per-file coverage gaps this PR opened
The layered-env reader gained an unreadable-layer path, a default reporter, and two absent-layer arms with no cases; the credential store gained two error paths that must not be mistaken for an absent file. The platform arms and the `linePos` guard cannot be reached from a POSIX test run — the first is covered by the native Windows job, the second only satisfies an optional type that `prettyErrors` always fills — so both carry a v8 ignore naming why.
This commit is contained in:
@@ -101,6 +101,7 @@ const GROUP_OTHER_BITS = 0o077
|
||||
* @throws when the file exists with group or other permission bits set.
|
||||
*/
|
||||
async function assertOwnerOnly(filename: string): Promise<void> {
|
||||
/* v8 ignore next -- native Windows coverage exercises the skip; POSIX covers the check */
|
||||
if (process.platform === 'win32') return
|
||||
let mode: number
|
||||
try {
|
||||
@@ -130,6 +131,7 @@ function isENOENT(error: unknown): boolean {
|
||||
*/
|
||||
function describeYamlError(error: YAMLError): string {
|
||||
const at = error.linePos?.[0]
|
||||
/* v8 ignore next -- `prettyErrors` populates linePos on every error; the guard answers its optional type */
|
||||
const where = at === undefined ? '' : ` at line ${String(at.line)}, column ${String(at.col)}`
|
||||
return `${error.code}${where}`
|
||||
}
|
||||
|
||||
@@ -179,6 +179,29 @@ describe('layer ladder', () => {
|
||||
.rejects.toThrow(/readable beyond its owner \(mode 644\)/)
|
||||
})
|
||||
|
||||
it('propagates a permission check that fails for a reason other than absence', async () => {
|
||||
const dir = await tempDir()
|
||||
const notADirectory = join(dir, 'occupied')
|
||||
await writeFile(notADirectory, 'a regular file\n')
|
||||
// An absent document is an empty store, but a path that cannot be
|
||||
// reached at all is a misconfiguration: the parent is a file, so the
|
||||
// check fails with ENOTDIR rather than concluding "no credentials yet".
|
||||
const ctx = new Context()
|
||||
await expect(ctx.plugin(CredentialsLocal, { path: join(notADirectory, '.credentials.yaml'), watch: false }))
|
||||
.rejects.toThrow(/ENOTDIR/)
|
||||
})
|
||||
|
||||
it('propagates a read that fails for a reason other than absence', async () => {
|
||||
const dir = await tempDir()
|
||||
const path = join(dir, '.credentials.yaml')
|
||||
// Owner-only, so the permission check passes, and unreadable as a file:
|
||||
// the store is present but cannot be parsed, which must fail the launch
|
||||
// rather than silently serve nothing.
|
||||
await mkdir(path, { mode: 0o700 })
|
||||
const ctx = new Context()
|
||||
await expect(ctx.plugin(CredentialsLocal, { path, watch: false })).rejects.toThrow(/EISDIR/)
|
||||
})
|
||||
|
||||
it('lets only the inherited environment shadow the store, read-only', async () => {
|
||||
const dir = await tempDir()
|
||||
const path = join(dir, '.credentials.yaml')
|
||||
|
||||
@@ -250,6 +250,7 @@ export class SettingsLocal extends Settings {
|
||||
throw new Error(`settings-local: invalid document at ${this.spec.filename}: ${
|
||||
document.errors.map((error) => {
|
||||
const at = error.linePos?.[0]
|
||||
/* v8 ignore next -- `prettyErrors` populates linePos on every error; the guard answers its optional type */
|
||||
return `${error.code}${at === undefined ? '' : ` at line ${String(at.line)}, column ${String(at.col)}`}`
|
||||
}).join('; ')}`)
|
||||
}
|
||||
|
||||
@@ -190,6 +190,113 @@ describe('loadLayeredEnv', () => {
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it('warns and continues when a layer exists but cannot be read', () => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
// A directory named `.env` is present-but-unreadable (EISDIR): unlike an
|
||||
// absent file, it is a real misconfiguration, so it is reported rather
|
||||
// than passed over in silence — and the other layers still load.
|
||||
mkdirSync(join(home, '.env'))
|
||||
writeFileSync(join(project, '.env'), `${NAMES[2]}=project-only\n`)
|
||||
clear()
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
const warn = vi.fn()
|
||||
try {
|
||||
const snapshot = loadLayeredEnv(NAME, project, warn)
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining(`${NAME}: failed to load .env`))
|
||||
expect(snapshot.layers).toEqual([
|
||||
{ source: 'process' },
|
||||
{ source: 'project-env', path: join(project, '.env') },
|
||||
])
|
||||
expect(process.env[NAMES[2]]).toBe('project-only')
|
||||
} finally {
|
||||
clear()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it('reports to stderr when the caller supplies no reporter', () => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
mkdirSync(join(home, '.env'))
|
||||
writeFileSync(join(project, '.env'), `${NAMES[2]}=project-only\n`)
|
||||
clear()
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
const write = vi.spyOn(process.stderr, 'write').mockReturnValue(true)
|
||||
try {
|
||||
const snapshot = loadLayeredEnv(NAME, project)
|
||||
expect(write).toHaveBeenCalledWith(expect.stringContaining(`${NAME}: failed to load .env`))
|
||||
expect(snapshot.layers).toEqual([
|
||||
{ source: 'process' },
|
||||
{ source: 'project-env', path: join(project, '.env') },
|
||||
])
|
||||
expect(process.env[NAMES[2]]).toBe('project-only')
|
||||
} finally {
|
||||
write.mockRestore()
|
||||
clear()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it('passes over an absent layer without reporting it', () => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
writeFileSync(join(project, '.env'), `${NAMES[2]}=project-only\n`)
|
||||
clear()
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
const warn = vi.fn()
|
||||
try {
|
||||
// No user `.env` exists, which is ordinary rather than a fault: the
|
||||
// layer is simply absent, and nothing is reported.
|
||||
const snapshot = loadLayeredEnv(NAME, project, warn)
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
expect(snapshot.layers).toEqual([
|
||||
{ source: 'process' },
|
||||
{ source: 'project-env', path: join(project, '.env') },
|
||||
])
|
||||
} finally {
|
||||
clear()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it('carries only the inherited environment when neither file exists', () => {
|
||||
const home = tmp()
|
||||
const project = tmp()
|
||||
clear()
|
||||
vi.stubEnv('DSH_HOME', home)
|
||||
vi.stubEnv('APP_BOOT_LAYERED_INHERITED', 'inherited')
|
||||
try {
|
||||
const snapshot = loadLayeredEnv(NAME, project, vi.fn())
|
||||
expect(snapshot.layers).toEqual([{ source: 'process' }])
|
||||
expect(snapshot.get('APP_BOOT_LAYERED_INHERITED')).toEqual({ value: 'inherited', source: 'process' })
|
||||
} finally {
|
||||
clear()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it('reads a harness home that is also the invocation directory exactly once', () => {
|
||||
const both = tmp()
|
||||
writeFileSync(join(both, '.env'), `${NAMES[2]}=one-file\n`)
|
||||
clear()
|
||||
vi.stubEnv('DSH_HOME', both)
|
||||
try {
|
||||
// One file cannot be two layers. It is the project layer, because that
|
||||
// is the more trusted of the two — reading it twice would otherwise
|
||||
// put the same path at two different ranks.
|
||||
const snapshot = loadLayeredEnv(NAME, both, vi.fn())
|
||||
expect(snapshot.layers).toEqual([
|
||||
{ source: 'process' },
|
||||
{ source: 'project-env', path: join(both, '.env') },
|
||||
])
|
||||
expect(snapshot.get(NAMES[2])).toEqual({ value: 'one-file', source: 'project-env', path: join(both, '.env') })
|
||||
} finally {
|
||||
clear()
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('installFailLoud', () => {
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface EnvironmentSnapshot {
|
||||
* @returns the key to store and look up by.
|
||||
*/
|
||||
function lookupKey(name: string): string {
|
||||
/* v8 ignore next -- native Windows coverage exercises the folding arm; POSIX covers the exact one */
|
||||
return process.platform === 'win32' ? name.toUpperCase() : name
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user