Repository: JoshuaKGoldberg/tslint-to-eslint-config (ts)

Issue:
Correctly test and stub out createNewConfiguration
The `createNewConfiguration` in `createNewConfiguration.ts` is neither covered by unit tests nor properly injected into the functions that use it. How unfortunate. Both of those things should happen. Right now it's happily printing `.eslintrc.json` during unit tests 😄 

Pull request description:
Stubbed out and tested createNewConfiguration
Fixes #9.

These tests will change for the better once #12 is added.

Patch:
diff --git a/.gitignore b/.gitignore
index cd964e9..14a0e6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,3 @@
 coverage/
 node_modules/
 src/**/*.js
-
-# https://github.com/JoshuaKGoldberg/tslint-to-eslint-config/issues/9
-.eslintrc.json
diff --git a/src/cli/main.ts b/src/cli/main.ts
index 5c93a7d..94e023f 100644
--- a/src/cli/main.ts
+++ b/src/cli/main.ts
@@ -4,6 +4,7 @@ import { EOL } from "os";
 import { promisify } from "util";
 
 import { convertConfig } from "../convertConfig";
+import { createNewConfiguration } from "../creation/createNewConfiguration";
 import { findTslintRules } from "../input/findTslintRules";
 import { TSLintToESLintSettings } from "../types";
 import { runCli } from "./runCli";
@@ -23,7 +24,14 @@ const fileExists = (filePath: string) => Promise.resolve(fs.existsSync(filePath)
 
 const runtime = {
     convertConfig: (settings: TSLintToESLintSettings) =>
-        convertConfig(settings, logger, ruleFinder, fileExists),
+        convertConfig({
+            createNewConfiguration: configConversionResults =>
+                createNewConfiguration(configConversionResults, fs.promises.writeFile),
+            fileExists,
+            logger,
+            ruleFinder,
+            settings,
+        }),
     logger,
     ruleFinder,
 };
diff --git a/src/convertConfig.ts b/src/convertConfig.ts
index 18df7ee..15e9b30 100644
--- a/src/convertConfig.ts
+++ b/src/convertConfig.ts
@@ -1,21 +1,31 @@
-import { createNewConfiguration } from "./creation/createNewConfiguration";
 import { FoundTSLintRules } from "./input/findTslintRules";
 import { ProcessLogger } from "./logger";
 import { reportConversionResults } from "./reportConversionResults";
-import { convertRules } from "./rules/convertRules";
+import { convertRules, ConfigConversionResults } from "./rules/convertRules";
 import { converters } from "./rules/converters";
 import { TSLintToESLintSettings, TSLintToESLintResult, ResultStatus } from "./types";
 
-export type RuleFinder = (config: string) => Promise<FoundTSLintRules | Error>;
+export type CreateNewConfiguration = (conversionResults: ConfigConversionResults) => Promise<void>;
 
 export type FileExists = (filePath: string) => Promise<boolean>;
 
-export const convertConfig = async (
-    settings: TSLintToESLintSettings,
-    logger: ProcessLogger,
-    ruleFinder: RuleFinder,
-    fileExists: FileExists,
-): Promise<TSLintToESLintResult> => {
+export type RuleFinder = (config: string) => Promise<FoundTSLintRules | Error>;
+
+export type ConvertConfigRequest = {
+    createNewConfiguration: CreateNewConfiguration;
+    fileExists: FileExists;
+    logger: ProcessLogger;
+    ruleFinder: RuleFinder;
+    settings: TSLintToESLintSettings;
+};
+
+export const convertConfig = async ({
+    createNewConfiguration,
+    fileExists,
+    logger,
+    ruleFinder,
+    settings,
+}: ConvertConfigRequest): Promise<TSLintToESLintResult> => {
     const { config = "./tslint.json" } = settings;
     if (!(await fileExists(config))) {
         return {
diff --git a/src/creation/createNewConfiguration.ts b/src/creation/createNewConfiguration.ts
index 50b3ab2..e637dce 100644
--- a/src/creation/createNewConfiguration.ts
+++ b/src/creation/createNewConfiguration.ts
@@ -1,9 +1,12 @@
-import * as fs from "fs";
-
 import { ConfigConversionResults } from "../rules/convertRules";
 import { formatConvertedRules } from "./formatConvertedRules";
 
-export const createNewConfiguration = async (conversionResults: ConfigConversionResults) => {
+export type WriteFile = (filePath: string, contents: string) => Promise<void>;
+
+export const createNewConfiguration = async (
+    conversionResults: ConfigConversionResults,
+    writeFile: WriteFile,
+) => {
     const output = {
         ...(conversionResults.missing.length && {
             plugins: ["@typescript-eslint/tslint"],
@@ -15,5 +18,5 @@ export const createNewConfiguration = async (conversionResults: ConfigConversion
         rules: formatConvertedRules(conversionResults),
     };
 
-    await fs.promises.writeFile(".eslintrc.json", JSON.stringify(output, undefined, 4));
+    await writeFile(".eslintrc.json", JSON.stringify(output, undefined, 4));
 };


Test patch:
diff --git a/src/convertConfig.test.ts b/src/convertConfig.test.ts
index 0e8740c..a089bd5 100644
--- a/src/convertConfig.test.ts
+++ b/src/convertConfig.test.ts
@@ -2,66 +2,72 @@ import { createStubLogger } from "./stubs";
 import { convertConfig } from "./convertConfig";
 import { ResultStatus } from "./types";
 
+const defaultRequest = {
+    settings: {},
+    logger: createStubLogger(),
+    ruleFinder: jest.fn().mockReturnValue(Promise.resolve(new Error())),
+    fileExists: jest.fn().mockReturnValue(Promise.resolve(true)),
+    createNewConfiguration: jest.fn().mockReturnValue(Promise.resolve()),
+};
+
 describe("convertConfig", () => {
     it("complains when the provided config file does not exist", async () => {
         // Arrange
-        const settings = {
-            config: "./stub/tslint.json",
+        const request = {
+            ...defaultRequest,
+            fileExists: jest.fn().mockReturnValue(Promise.resolve(false)),
         };
-        const logger = createStubLogger();
-        const ruleFinder = jest.fn().mockReturnValue(Promise.resolve(new Error()));
-        const fileExists = jest.fn().mockReturnValue(Promise.resolve(false));
 
         // Act
-        const result = await convertConfig(settings, logger, ruleFinder, fileExists);
+        const result = await convertConfig(request);
 
         // Assert
         expect(result).toEqual({
-            complaint: `${settings.config} does not seem to exist.`,
+            complaint: `./tslint.json does not seem to exist.`,
             status: ResultStatus.ConfigurationError,
         });
     });
 
     it("searches for settings.config when settings.config is provided", async () => {
         // Arrange
-        const settings = {
-            config: "./stub/tslint.json",
+        const request = {
+            ...defaultRequest,
+            settings: {
+                config: "./stub/tslint.json",
+            },
         };
-        const logger = createStubLogger();
-        const ruleFinder = jest.fn().mockReturnValue(Promise.resolve(new Error()));
-        const fileExists = jest.fn().mockReturnValue(Promise.resolve(true));
 
         // Act
-        await convertConfig(settings, logger, ruleFinder, fileExists);
+        await convertConfig(request);
 
         // Assert
-        expect(ruleFinder).toHaveBeenLastCalledWith(settings.config);
+        expect(request.ruleFinder).toHaveBeenLastCalledWith(request.settings.config);
     });
 
     it("searches for ./tslint.json by default when no settings.config is provided", async () => {
         // Arrange
-        const settings = {};
-        const logger = createStubLogger();
-        const ruleFinder = jest.fn().mockReturnValue(Promise.resolve(new Error()));
-        const fileExists = jest.fn().mockReturnValue(Promise.resolve(true));
+        const request = {
+            ...defaultRequest,
+            settings: {},
+        };
 
         // Act
-        await convertConfig(settings, logger, ruleFinder, fileExists);
+        await convertConfig(request);
 
         // Assert
-        expect(ruleFinder).toHaveBeenLastCalledWith("./tslint.json");
+        expect(request.ruleFinder).toHaveBeenLastCalledWith("./tslint.json");
     });
 
     it("returns a failure result when ruleFinder returns an error", async () => {
         // Arrange
-        const settings = {};
-        const logger = createStubLogger();
         const error = new Error("oh no");
-        const ruleFinder = jest.fn().mockReturnValue(Promise.resolve(error));
-        const fileExists = jest.fn().mockReturnValue(Promise.resolve(true));
+        const request = {
+            ...defaultRequest,
+            ruleFinder: jest.fn().mockReturnValue(Promise.resolve(error)),
+        };
 
         // Act
-        const result = await convertConfig(settings, logger, ruleFinder, fileExists);
+        const result = await convertConfig(request);
 
         // Assert
         expect(result).toEqual({
@@ -72,20 +78,20 @@ describe("convertConfig", () => {
 
     it("creates a new configuration when ruleFinder returns rules", async () => {
         // Arrange
-        const settings = {};
-        const logger = createStubLogger();
-        const ruleFinder = jest.fn().mockReturnValue({
-            rules: {
-                "sample-rule": {
-                    ruleArguments: ["one", "two"],
-                    ruleName: "sample-rule",
+        const request = {
+            ...defaultRequest,
+            ruleFinder: jest.fn().mockReturnValue({
+                rules: {
+                    "sample-rule": {
+                        ruleArguments: ["one", "two"],
+                        ruleName: "sample-rule",
+                    },
                 },
-            },
-        });
-        const fileExists = jest.fn().mockReturnValue(Promise.resolve(true));
+            }),
+        };
 
         // Act
-        const result = await convertConfig(settings, logger, ruleFinder, fileExists);
+        const result = await convertConfig(request);
 
         // Assert
         expect(result).toEqual({
diff --git a/src/creation/createNewConfiguration.test.ts b/src/creation/createNewConfiguration.test.ts
new file mode 100644
index 0000000..c684305
--- /dev/null
+++ b/src/creation/createNewConfiguration.test.ts
@@ -0,0 +1,68 @@
+import { emptyConversionResults } from "../stubs";
+import { createNewConfiguration } from "./createNewConfiguration";
+import { ConfigConversionResults } from "../rules/convertRules";
+
+describe("createNewConfiguration", () => {
+    it("writes only formatted rules when there are no missing rules", async () => {
+        // Arrange
+        const conversionResults: ConfigConversionResults = {
+            ...emptyConversionResults,
+            converted: new Map(),
+        };
+        const writeFile = jest.fn().mockReturnValue(Promise.resolve());
+
+        // Act
+        await createNewConfiguration(conversionResults, writeFile);
+
+        // Assert
+        expect(writeFile).toHaveBeenLastCalledWith(
+            ".eslintrc.json",
+            JSON.stringify({ rules: {} }, undefined, 4),
+        );
+    });
+
+    it("includes typescript-eslint plugin settings when there are missing rules", async () => {
+        // Arrange
+        const conversionResults: ConfigConversionResults = {
+            ...emptyConversionResults,
+            converted: new Map(),
+            missing: [
+                {
+                    ruleArguments: [],
+                    ruleName: "tslint-rule-one",
+                    ruleSeverity: "error",
+                },
+            ],
+        };
+        const writeFile = jest.fn().mockReturnValue(Promise.resolve());
+
+        // Act
+        await createNewConfiguration(conversionResults, writeFile);
+
+        // Assert
+        expect(writeFile).toHaveBeenLastCalledWith(
+            ".eslintrc.json",
+            JSON.stringify(
+                {
+                    plugins: ["@typescript-eslint/tslint"],
+                    parser: "@typescript-eslint/parser",
+                    parserOptions: {
+                        project: "tsconfig.json",
+                    },
+                    rules: {
+                        "@typescript-eslint/tslint/config": [
+                            "error",
+                            {
+                                rules: {
+                                    "tslint-rule-one": true,
+                                },
+                            },
+                        ],
+                    },
+                },
+                undefined,
+                4,
+            ),
+        );
+    });
+});