nps.test.ts•13.2 kB
import fs from 'fs'
import readline from 'readline'
import { PassThrough } from 'stream'
import { CommandState } from '../utils/commandState'
import { createSafeReadlineProxy, handleNpsSurveyImpl } from '../utils/nps/survey'
const currentDate = new Date('2022-01-01T00:00:00.000Z')
const laterDate = new Date('2023-01-01T00:00:00.000Z')
const earlierDate = new Date('2021-01-01T00:00:00.000Z')
const evenEarlierDate = new Date('2020-01-01T00:00:00.000Z')
const longTimeUserCommandState: CommandState = { firstCommandTimestamp: '2019-01-01T00:00:00.000Z' }
describe('nps survey', () => {
const originalEnv = { ...process.env }
const restoreEnv = () => {
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) {
delete process.env[key]
}
}
for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
}
let mockRead: jest.SpyInstance
let mockWrite: jest.SpyInstance
let mockExists: jest.SpyInstance
beforeEach(() => {
restoreEnv()
for (const key of Object.keys(process.env)) {
delete process.env[key]
}
})
afterEach(() => {
mockRead?.mockRestore()
mockWrite?.mockRestore()
mockExists?.mockRestore()
})
afterAll(() => {
restoreEnv()
})
it('should exit immediately if running in CI', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockImplementation()
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn()
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
process.env.CI = 'true'
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(0)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(0)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should exit immediately if running in a Podman container', async () => {
mockExists = jest.spyOn(fs, 'existsSync').mockImplementation((path) => {
return path === '/run/.containerenv'
})
mockRead = jest.spyOn(fs.promises, 'readFile').mockImplementation()
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn()
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(0)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(0)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should exit immediately if running in a Docker container', async () => {
mockExists = jest.spyOn(fs, 'existsSync').mockImplementation((path) => {
return path === '/.dockerenv'
})
mockRead = jest.spyOn(fs.promises, 'readFile').mockImplementation()
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn()
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(0)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(0)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should exit immediately if running in a Kubernetes pod', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockImplementation()
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn()
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
process.env.KUBERNETES_SERVICE_HOST = '10.96.0.1'
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(0)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(0)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should exit immediately if running in a pre-commit git hook', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockImplementation()
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn()
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
process.env.GIT_EXEC_PATH = '/nix/store/9z3jhc0rlj3zaw8nd1zka9vli6w0q11g-git-2.47.2/libexec/git-core'
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(0)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(0)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should exit immediately if running in a post-install npm hook or similar', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockImplementation()
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn()
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
process.env.npm_command = 'install'
process.env.npm_lifecycle_event = 'prepare'
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(0)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(0)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should read the config and exit when the current survey has been acknowledged', async () => {
mockRead = jest
.spyOn(fs.promises, 'readFile')
.mockResolvedValue(
JSON.stringify({ acknowledgedTimeframe: { start: earlierDate.toISOString(), end: laterDate.toISOString() } }),
)
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn()
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(1)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(0)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should check the status if there is no config and exit if there is no survey', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockRejectedValue({ code: 'ENOENT' })
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest
.fn()
.mockResolvedValue({ currentTimeframe: { start: evenEarlierDate.toISOString(), end: earlierDate.toISOString() } })
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(1)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(1)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should check the status if the acknowledged survey has expired', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockResolvedValue(
JSON.stringify({
acknowledgedTimeframe: { start: evenEarlierDate.toISOString(), end: earlierDate.toISOString() },
}),
)
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn().mockResolvedValue({})
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn()
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(1)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(1)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should exit if the status is undefined', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockRejectedValue({ code: 'ENOENT' })
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const status = jest.fn().mockResolvedValue({})
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn().mockReturnValue(Promise.resolve())
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(1)
expect(mockWrite).toHaveBeenCalledTimes(0)
expect(status).toHaveBeenCalledTimes(1)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should exit if this command is within 24 hours of the first command issued', async () => {
const status = jest.fn().mockResolvedValue({})
const readline = {
question: jest.fn(),
write: jest.fn(),
}
const capture = jest.fn().mockReturnValue(Promise.resolve())
const commandState = {
firstCommandTimestamp: new Date(
Date.now() - ((Math.random() * Number.MAX_SAFE_INTEGER) % (23 * 60 * 60 * 1000)),
).toISOString(),
}
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, commandState)
expect(status).toHaveBeenCalledTimes(0)
expect(readline.question).toHaveBeenCalledTimes(0)
expect(capture).toHaveBeenCalledTimes(0)
})
it('should prompt the user if the survey is active and update the config', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockRejectedValue({ code: 'ENOENT' })
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const currentTimeframe = { start: earlierDate.toISOString(), end: laterDate.toISOString() }
const status = jest.fn().mockResolvedValue({ currentTimeframe })
const readline = {
question: jest.fn().mockResolvedValueOnce('5').mockResolvedValueOnce('Great!'),
write: jest.fn(),
}
const capture = jest.fn().mockReturnValue(Promise.resolve())
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalled()
expect(mockWrite).toHaveBeenLastCalledWith(
expect.anything(),
JSON.stringify({ acknowledgedTimeframe: currentTimeframe }),
)
expect(status).toHaveBeenCalledTimes(1)
expect(readline.question).toHaveBeenCalledTimes(2)
expect(readline.write).toHaveBeenCalledWith('Thanks for your feedback!\n')
expect(capture).toHaveBeenCalledWith(expect.anything(), 'NPS feedback', { rating: 5, feedback: 'Great!' })
})
it('should allow the user to skip the survey and still update the config', async () => {
mockRead = jest.spyOn(fs.promises, 'readFile').mockRejectedValue({ code: 'ENOENT' })
mockWrite = jest.spyOn(fs.promises, 'writeFile').mockImplementation()
const currentTimeframe = { start: earlierDate.toISOString(), end: laterDate.toISOString() }
const status = jest.fn().mockResolvedValue({ currentTimeframe })
const readline = {
question: jest.fn().mockResolvedValueOnce('no'),
write: jest.fn(),
}
const capture = jest.fn().mockReturnValue(Promise.resolve())
await handleNpsSurveyImpl(currentDate, { status }, readline, { capture }, longTimeUserCommandState)
expect(mockRead).toHaveBeenCalledTimes(1)
expect(mockWrite).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify({ acknowledgedTimeframe: currentTimeframe }),
)
expect(status).toHaveBeenCalledTimes(1)
expect(readline.question).toHaveBeenCalledTimes(1)
expect(readline.write).toHaveBeenCalledWith('Not received a valid rating. Exiting the survey.\n')
expect(capture).toHaveBeenCalledTimes(0)
})
})
describe('createSafeReadlineProxy', () => {
it('should handle an input stream that closes', async () => {
const input = new PassThrough()
const output = new PassThrough()
const rl = readline.promises.createInterface({
input,
output,
})
const proxy = createSafeReadlineProxy(rl)
process.nextTick(() => proxy.write('answer\n'))
await expect(proxy.question('question')).resolves.toBe('answer')
rl.close()
expect(() => proxy.question('question')).toThrow('This operation was aborted')
})
})