From 1405e22f89eed332ae30ffe5cea1d676bac78a2a Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 19 Jan 2025 11:27:19 +0100 Subject: [PATCH 1/7] test(import/mime): add tests --- src/services/import/mime.spec.ts | 146 +++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/services/import/mime.spec.ts diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts new file mode 100644 index 000000000..bc2497ecc --- /dev/null +++ b/src/services/import/mime.spec.ts @@ -0,0 +1,146 @@ +import { describe, it, expect } from "vitest"; +import mimeService from "./mime.js"; + +type TestCase any, W> = [desc: string, fnParams: Parameters, expected: W]; + +describe("#getMime", () => { + // prettier-ignore + const testCases: TestCase[] = [ + [ + "Dockerfile should be handled correctly", + ["Dockerfile"], "text/x-dockerfile" + ], + + [ + "File extension that is defined in EXTENSION_TO_MIME", + ["test.py"], "text/x-python" + ], + + [ + "File extension that is not defined in EXTENSION_TO_MIME should use mimeTypes.lookup", + ["test.zip"], "application/zip" + ], + + [ + "unknown MIME type not recognized by mimeTypes.lookup", + ["test.fake"], false + ], + ]; + + testCases.forEach((testCase) => { + const [testDesc, fnParams, expected] = testCase; + it(`${testDesc}: '${fnParams} should return '${expected}'`, () => { + const actual = mimeService.getMime(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); + +describe("#getType", () => { + // prettier-ignore + const testCases: TestCase[] = [ + [ + "w/ no import options set and mime type empty – it should return 'file'", + [{}, ""], "file" + ], + + [ + "w/ no import options set and non-text or non-code mime type – it should return 'file'", + [{}, "application/zip"], "file" + ], + + [ + "w/ import options set and an image mime type – it should return 'image'", + [{}, "image/jpeg"], "image" + ], + + [ + "w/ image mime type and codeImportedAsCode: true – it should still return 'image'", + [{codeImportedAsCode: true}, "image/jpeg"], "image" + ], + + [ + "w/ image mime type and textImportedAsText: true – it should still return 'image'", + [{textImportedAsText: true}, "image/jpeg"], "image" + ], + + [ + "w/ codeImportedAsCode: true and a mime type that is in CODE_MIME_TYPES – it should return 'code'", + [{codeImportedAsCode: true}, "text/css"], "code" + ], + + [ + "w/ codeImportedAsCode: false and a mime type that is in CODE_MIME_TYPES – it should return 'file' not 'code'", + [{codeImportedAsCode: false}, "text/css"], "file" + ], + + [ + "w/ textImportedAsText: true and 'text/html' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/html"], "text" + ], + + [ + "w/ textImportedAsText: true and 'text/markdown' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/markdown"], "text" + ], + + [ + "w/ textImportedAsText: true and 'text/x-markdown' mime type – it should return 'text'", + [{textImportedAsText: true}, "text/x-markdown"], "text" + ], + + [ + "w/ textImportedAsText: false and 'text/x-markdown' mime type – it should return 'file'", + [{textImportedAsText: false}, "text/x-markdown"], "file" + ], + + [ + "w/ textImportedAsText: false and 'text/html' mime type – it should return 'file'", + [{textImportedAsText: false}, "text/html"], "file" + ], + + ] + + testCases.forEach((testCase) => { + const [desc, fnParams, expected] = testCase; + it(desc, () => { + const actual = mimeService.getType(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); + +describe("#normalizeMimeType", () => { + // prettier-ignore + const testCases: TestCase[] = [ + + [ + "empty mime should return undefined", + [""], undefined + ], + [ + "a mime that's defined in CODE_MIME_TYPES should return the same mime", + ["text/x-python"], "text/x-python" + ], + [ + "a mime (with capitalization inconsistencies) that's defined in CODE_MIME_TYPES should return the same mime in lowercase", + ["text/X-pYthOn"], "text/x-python" + ], + [ + "a mime that's non defined in CODE_MIME_TYPES should return undefined", + ["application/zip"], undefined + ], + [ + "a mime that's defined in CODE_MIME_TYPES with a 'rewrite rule' should return the rewritten mime", + ["text/markdown"], "text/x-markdown" + ] + ]; + + testCases.forEach((testCase) => { + const [desc, fnParams, expected] = testCase; + it(desc, () => { + const actual = mimeService.normalizeMimeType(...fnParams); + expect(actual).toEqual(expected); + }); + }); +}); From 815929c3768545c57be9bca6c5fe7831cb1e4d41 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 19 Jan 2025 14:50:59 +0100 Subject: [PATCH 2/7] refactor(import/mime): split CODE_MIME_TYPES Record into two separate objects CODE_MIME_TYPES -> as a Set -> as we only care about the existance of those types CODE_MIME_TYPES_OVERRIDE -> as a Map with those keys and the "overwrite" values as associated value -> this way we don't have to unnecessarily store additional boolean values for everything *but* those hand ful of mime types -> also I've sorted the items alphabetically, while I was at it --- src/services/import/mime.ts | 88 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 10463c242..8fe97a5ea 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -4,45 +4,48 @@ import mimeTypes from "mime-types"; import path from "path"; import type { TaskData } from "../task_context_interface.js"; -const CODE_MIME_TYPES: Record = { - "text/plain": true, - "text/x-csrc": true, - "text/x-c++src": true, - "text/x-csharp": true, - "text/x-clojure": true, - "text/css": true, - "text/x-dockerfile": true, - "text/x-erlang": true, - "text/x-feature": true, - "text/x-go": true, - "text/x-groovy": true, - "text/x-haskell": true, - "text/html": true, - "message/http": true, - "text/x-java": true, - "application/javascript": "application/javascript;env=frontend", - "application/x-javascript": "application/javascript;env=frontend", - "application/json": true, - "text/x-kotlin": true, - "text/x-stex": true, - "text/x-lua": true, +const CODE_MIME_TYPES = new Set([ + "application/json", + "message/http", + "text/css", + "text/html", + "text/plain", + "text/x-clojure", + "text/x-csharp", + "text/x-c++src", + "text/x-csrc", + "text/x-dockerfile", + "text/x-erlang", + "text/x-feature", + "text/x-go", + "text/x-groovy", + "text/x-haskell", + "text/x-java", + "text/x-kotlin", + "text/x-lua", + "text/x-markdown", + "text/xml", + "text/x-objectivec", + "text/x-pascal", + "text/x-perl", + "text/x-php", + "text/x-python", + "text/x-ruby", + "text/x-rustsrc", + "text/x-scala", + "text/x-sh", + "text/x-sql", + "text/x-stex", + "text/x-swift", + "text/x-yaml" +]); + +const CODE_MIME_TYPES_OVERRIDE = new Map([ + ["application/javascript", "application/javascript;env=frontend"], + ["application/x-javascript", "application/javascript;env=frontend"], // possibly later migrate to text/markdown as primary MIME - "text/markdown": "text/x-markdown", - "text/x-markdown": true, - "text/x-objectivec": true, - "text/x-pascal": true, - "text/x-perl": true, - "text/x-php": true, - "text/x-python": true, - "text/x-ruby": true, - "text/x-rustsrc": true, - "text/x-scala": true, - "text/x-sh": true, - "text/x-sql": true, - "text/x-swift": true, - "text/xml": true, - "text/x-yaml": true -}; + ["text/markdown", "text/x-markdown"] +]); // extensions missing in mime-db const EXTENSION_TO_MIME: Record = { @@ -85,7 +88,7 @@ function getType(options: TaskData, mime: string) { if (options.textImportedAsText && (mime === "text/html" || ["text/markdown", "text/x-markdown"].includes(mime))) { return "text"; - } else if (options.codeImportedAsCode && mime in CODE_MIME_TYPES) { + } else if (options.codeImportedAsCode && CODE_MIME_TYPES.has(mime)) { return "code"; } else if (mime.startsWith("image/")) { return "image"; @@ -96,12 +99,11 @@ function getType(options: TaskData, mime: string) { function normalizeMimeType(mime: string) { mime = mime ? mime.toLowerCase() : ""; - const mappedMime = CODE_MIME_TYPES[mime]; - if (mappedMime === true) { + if (CODE_MIME_TYPES.has(mime)) { return mime; - } else if (typeof mappedMime === "string") { - return mappedMime; + } else if (CODE_MIME_TYPES_OVERRIDE.get(mime)) { + return CODE_MIME_TYPES_OVERRIDE.get(mime); } return undefined; From 91ae4b629e7084528c43a6d2f3738866e530c917 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:18:35 +0100 Subject: [PATCH 3/7] refactor(import/mime): simplify normalizeMimeType --- src/services/import/mime.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 8fe97a5ea..5476da78c 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -40,7 +40,7 @@ const CODE_MIME_TYPES = new Set([ "text/x-yaml" ]); -const CODE_MIME_TYPES_OVERRIDE = new Map([ +const CODE_MIME_TYPES_OVERRIDE = new Map([ ["application/javascript", "application/javascript;env=frontend"], ["application/x-javascript", "application/javascript;env=frontend"], // possibly later migrate to text/markdown as primary MIME @@ -98,15 +98,12 @@ function getType(options: TaskData, mime: string) { } function normalizeMimeType(mime: string) { - mime = mime ? mime.toLowerCase() : ""; + const mimeLc = mime.toLowerCase(); - if (CODE_MIME_TYPES.has(mime)) { - return mime; - } else if (CODE_MIME_TYPES_OVERRIDE.get(mime)) { - return CODE_MIME_TYPES_OVERRIDE.get(mime); - } - - return undefined; + //prettier-ignore + return CODE_MIME_TYPES.has(mimeLc) + ? mimeLc + : CODE_MIME_TYPES_OVERRIDE.get(mimeLc); } export default { From 6a0edb68de88b0be543e54bd97a5cd78c2cb7ef9 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:22:31 +0100 Subject: [PATCH 4/7] refactor(import/mime): simplify getType --- src/services/import/mime.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index 5476da78c..a1b092556 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -84,16 +84,20 @@ function getMime(fileName: string) { } function getType(options: TaskData, mime: string) { - mime = mime ? mime.toLowerCase() : ""; + const mimeLc = mime?.toLowerCase(); - if (options.textImportedAsText && (mime === "text/html" || ["text/markdown", "text/x-markdown"].includes(mime))) { - return "text"; - } else if (options.codeImportedAsCode && CODE_MIME_TYPES.has(mime)) { - return "code"; - } else if (mime.startsWith("image/")) { - return "image"; - } else { - return "file"; + switch (true) { + case options.textImportedAsText && ["text/html", "text/markdown", "text/x-markdown"].includes(mimeLc): + return "text"; + + case options.codeImportedAsCode && CODE_MIME_TYPES.has(mimeLc): + return "code"; + + case mime.startsWith("image/"): + return "image"; + + default: + return "file"; } } From 4e59f58ce683d2f6edd0e3df1d94aef1c5f8f4a9 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:34:22 +0100 Subject: [PATCH 5/7] refactor(import/mime): simplify getMime --- src/services/import/mime.ts | 51 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/services/import/mime.ts b/src/services/import/mime.ts index a1b092556..9930c9010 100644 --- a/src/services/import/mime.ts +++ b/src/services/import/mime.ts @@ -48,39 +48,38 @@ const CODE_MIME_TYPES_OVERRIDE = new Map([ ]); // extensions missing in mime-db -const EXTENSION_TO_MIME: Record = { - ".c": "text/x-csrc", - ".cs": "text/x-csharp", - ".clj": "text/x-clojure", - ".erl": "text/x-erlang", - ".hrl": "text/x-erlang", - ".feature": "text/x-feature", - ".go": "text/x-go", - ".groovy": "text/x-groovy", - ".hs": "text/x-haskell", - ".lhs": "text/x-haskell", - ".http": "message/http", - ".kt": "text/x-kotlin", - ".m": "text/x-objectivec", - ".py": "text/x-python", - ".rb": "text/x-ruby", - ".scala": "text/x-scala", - ".swift": "text/x-swift" -}; +const EXTENSION_TO_MIME = new Map([ + [".c", "text/x-csrc"], + [".cs", "text/x-csharp"], + [".clj", "text/x-clojure"], + [".erl", "text/x-erlang"], + [".hrl", "text/x-erlang"], + [".feature", "text/x-feature"], + [".go", "text/x-go"], + [".groovy", "text/x-groovy"], + [".hs", "text/x-haskell"], + [".lhs", "text/x-haskell"], + [".http", "message/http"], + [".kt", "text/x-kotlin"], + [".m", "text/x-objectivec"], + [".py", "text/x-python"], + [".rb", "text/x-ruby"], + [".scala", "text/x-scala"], + [".swift", "text/x-swift"] +]); /** @returns false if MIME is not detected */ function getMime(fileName: string) { - if (fileName.toLowerCase() === "dockerfile") { + const fileNameLc = fileName?.toLowerCase(); + + if (fileNameLc === "dockerfile") { return "text/x-dockerfile"; } - const ext = path.extname(fileName).toLowerCase(); + const ext = path.extname(fileNameLc); + const mimeFromExt = EXTENSION_TO_MIME.get(ext); - if (ext in EXTENSION_TO_MIME) { - return EXTENSION_TO_MIME[ext]; - } - - return mimeTypes.lookup(fileName); + return mimeFromExt || mimeTypes.lookup(fileNameLc); } function getType(options: TaskData, mime: string) { From 4be675c4e126503b755cb4a59bf84f8c84ce9b97 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 08:34:52 +0100 Subject: [PATCH 6/7] test(import/mime): add additional test case for getMime --- src/services/import/mime.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/import/mime.spec.ts b/src/services/import/mime.spec.ts index bc2497ecc..acebd94f7 100644 --- a/src/services/import/mime.spec.ts +++ b/src/services/import/mime.spec.ts @@ -16,6 +16,11 @@ describe("#getMime", () => { ["test.py"], "text/x-python" ], + [ + "File extension with inconsisten capitalization that is defined in EXTENSION_TO_MIME", + ["test.gRoOvY"], "text/x-groovy" + ], + [ "File extension that is not defined in EXTENSION_TO_MIME should use mimeTypes.lookup", ["test.zip"], "application/zip" From ca8146413a81c8fa83cd42178777ac34ed2ee8c7 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 20 Jan 2025 18:57:43 +0100 Subject: [PATCH 7/7] test(data_dir): fix flaky getPlatformAppDataDir test on Windows Delete the provided process.env.APPDATA on Windows, so that we can use our own values (one of which is "undefined", which was causing the getPlatformAppDataDir to fallback to the "real" process.env.APPDATA -> causing failing test, when run on Windows --- src/services/data_dir.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/data_dir.spec.ts b/src/services/data_dir.spec.ts index 37958b185..4c94cbe94 100644 --- a/src/services/data_dir.spec.ts +++ b/src/services/data_dir.spec.ts @@ -77,6 +77,11 @@ describe("data_dir.ts unit tests", async () => { ["w/ darwin it should return '~/Library/Application Support'", ["darwin", undefined], "/Users/mock/Library/Application Support", "/Users/mock"] ]; + beforeEach(() => { + // make sure OS does not set its own process.env.APPDATA, so that we can use our own supplied value + delete process.env.APPDATA; + }); + testCases.forEach((testCase) => { const [testDescription, fnValues, expected, osHomedirMockValue] = testCase; return it(testDescription, () => {