From 950bff4da8039df418a9723adc17e7a114d2a3f6 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 28 Jun 2021 12:43:08 +0200 Subject: [PATCH] fix(cli-repl): store config using EJSON MONGOSH-850 EJSON comes with support for values like `Infinity`, unlike plain JSON. --- packages/cli-repl/src/cli-repl.spec.ts | 19 ++++++++++++++----- .../cli-repl/src/config-directory.spec.ts | 5 ++++- packages/cli-repl/src/config-directory.ts | 7 +++++-- packages/cli-repl/test/e2e.spec.ts | 4 +++- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/cli-repl/src/cli-repl.spec.ts b/packages/cli-repl/src/cli-repl.spec.ts index 401c6b030f..5a9a3ace4b 100644 --- a/packages/cli-repl/src/cli-repl.spec.ts +++ b/packages/cli-repl/src/cli-repl.spec.ts @@ -1,5 +1,4 @@ import { MongoshInternalError } from '@mongosh/errors'; -import bson from 'bson'; import { once } from 'events'; import { promises as fs } from 'fs'; import http from 'http'; @@ -11,6 +10,8 @@ import { expect, fakeTTYProps, readReplLogfile, tick, useTmpdir, waitBus, waitCo import { eventually } from '../../../testing/eventually'; import CliRepl, { CliReplOptions } from './cli-repl'; import { CliReplErrors } from './error-codes'; +import { bson } from '@mongosh/service-provider-core'; +const { EJSON } = bson; const delay = promisify(setTimeout); @@ -109,12 +110,12 @@ describe('CliRepl', () => { await evalComplete; // eval-complete includes the fs.writeFile() call. const content = await fs.readFile(path.join(tmpdir.path, 'config'), { encoding: 'utf8' }); - expect(JSON.parse(content).enableTelemetry).to.be.false; + expect((EJSON.parse(content) as any).enableTelemetry).to.be.false; }); it('does not store config options on disk that have not been changed', async() => { let content = await fs.readFile(path.join(tmpdir.path, 'config'), { encoding: 'utf8' }); - expect(Object.keys(JSON.parse(content))).to.deep.equal([ + expect(Object.keys(EJSON.parse(content))).to.deep.equal([ 'userId', 'enableTelemetry', 'disableGreetingMessage' ]); @@ -122,7 +123,7 @@ describe('CliRepl', () => { await waitEval(cliRepl.bus); content = await fs.readFile(path.join(tmpdir.path, 'config'), { encoding: 'utf8' }); - expect(Object.keys(JSON.parse(content))).to.deep.equal([ + expect(Object.keys(EJSON.parse(content))).to.deep.equal([ 'userId', 'enableTelemetry', 'disableGreetingMessage', 'inspectDepth' ]); @@ -130,11 +131,19 @@ describe('CliRepl', () => { cliRepl = new CliRepl(cliReplOptions); await cliRepl.start('', {}); content = await fs.readFile(path.join(tmpdir.path, 'config'), { encoding: 'utf8' }); - expect(Object.keys(JSON.parse(content))).to.deep.equal([ + expect(Object.keys(EJSON.parse(content))).to.deep.equal([ 'userId', 'enableTelemetry', 'disableGreetingMessage', 'inspectDepth' ]); }); + it('store config options on disk cannot be represented in traditional JSON', async() => { + input.write('config.set("inspectDepth", Infinity)\n'); + + await waitEval(cliRepl.bus); + const content = await fs.readFile(path.join(tmpdir.path, 'config'), { encoding: 'utf8' }); + expect((EJSON.parse(content) as any).inspectDepth).equal(Infinity); + }); + it('emits exit when asked to, Node.js-style', async() => { input.write('.exit\n'); await exitPromise; diff --git a/packages/cli-repl/src/config-directory.spec.ts b/packages/cli-repl/src/config-directory.spec.ts index ba0c7b6ce6..5402fc4123 100644 --- a/packages/cli-repl/src/config-directory.spec.ts +++ b/packages/cli-repl/src/config-directory.spec.ts @@ -6,10 +6,13 @@ import { promisify } from 'util'; import chai, { expect } from 'chai'; import sinon from 'ts-sinon'; import sinonChai from 'sinon-chai'; +import { bson } from '@mongosh/service-provider-core'; +const { EJSON } = bson; chai.use(sinonChai); class ExampleConfig { someProperty = 42; + someOtherProperty?: number = Infinity; } describe('home directory management', () => { @@ -62,7 +65,7 @@ describe('home directory management', () => { const configPath = manager.path(); await manager.writeConfigFile(new ExampleConfig()); const contents = await fs.readFile(configPath, { encoding: 'utf8' }); - expect(JSON.parse(contents)).to.deep.equal(new ExampleConfig()); + expect(EJSON.parse(contents)).to.deep.equal(new ExampleConfig()); expect(onError).to.not.have.been.called; expect(onNewConfig).to.not.have.been.called; diff --git a/packages/cli-repl/src/config-directory.ts b/packages/cli-repl/src/config-directory.ts index bd81582eed..469c14dfa9 100644 --- a/packages/cli-repl/src/config-directory.ts +++ b/packages/cli-repl/src/config-directory.ts @@ -2,6 +2,8 @@ import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import { EventEmitter } from 'events'; +import { bson } from '@mongosh/service-provider-core'; +const { EJSON } = bson; export type ShellHomePaths = { shellRoamingDataPath: string; @@ -75,7 +77,7 @@ export class ConfigManager extends EventEmitter { if (fd !== undefined) { // Not the first access. Read the file and return it. try { - const config: Config = JSON.parse(await fd.readFile({ encoding: 'utf8' })); + const config: Config = EJSON.parse(await fd.readFile({ encoding: 'utf8' })) as any; this.emit('update-config', config); return { ...defaultConfig, ...config }; } catch (err) { @@ -99,7 +101,8 @@ export class ConfigManager extends EventEmitter { async writeConfigFile(config: Config): Promise { await this.shellHomeDirectory.ensureExists(); try { - await fs.writeFile(this.path(), JSON.stringify(config), { mode: 0o600 }); + // TODO: { relaxed: false } can be removed once NODE-3390 is done. + await fs.writeFile(this.path(), EJSON.stringify(config, { relaxed: false }), { mode: 0o600 }); } catch (err) { this.emit('error', err); throw err; diff --git a/packages/cli-repl/test/e2e.spec.ts b/packages/cli-repl/test/e2e.spec.ts index 8a23dd21a9..4624c43025 100644 --- a/packages/cli-repl/test/e2e.spec.ts +++ b/packages/cli-repl/test/e2e.spec.ts @@ -9,6 +9,8 @@ import { promisify } from 'util'; import rimraf from 'rimraf'; import path from 'path'; import { readReplLogfile } from './repl-helpers'; +import { bson } from '@mongosh/service-provider-core'; +const { EJSON } = bson; describe('e2e', function() { const testServer = startTestServer('shared'); @@ -682,7 +684,7 @@ describe('e2e', function() { configPath = path.resolve(homedir, '.mongodb', 'mongosh', 'config'); historyPath = path.resolve(homedir, '.mongodb', 'mongosh', 'mongosh_repl_history'); } - readConfig = async() => JSON.parse(await fs.readFile(configPath, 'utf8')); + readConfig = async() => EJSON.parse(await fs.readFile(configPath, 'utf8')); readLogfile = async() => readReplLogfile(logPath); startTestShell = async(...extraArgs: string[]) => { const shell = TestShell.start({