From 49a7d5188d3ff97c5dd6757a15be83eb66a37f2d Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Tue, 24 Sep 2024 12:28:35 -0400 Subject: [PATCH 1/9] Add tests for cms/functions --- lib/__tests__/functions.test.ts | 159 ++++++++++++++++++++++++++++++++ lib/cms/functions.ts | 6 +- 2 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 lib/__tests__/functions.test.ts diff --git a/lib/__tests__/functions.test.ts b/lib/__tests__/functions.test.ts new file mode 100644 index 0000000..887a26c --- /dev/null +++ b/lib/__tests__/functions.test.ts @@ -0,0 +1,159 @@ +import fs, { PathLike } from 'fs-extra'; +import findup from 'findup-sync'; +import { getCwd } from '../path'; +import { downloadGithubRepoContents } from '../github'; +import { logger } from '../logger'; +import { + createFunction, + isObjectOrFunction, + createEndpoint, + createConfig, +} from '../cms/functions'; + +jest.mock('fs-extra'); +jest.mock('findup-sync'); +jest.mock('../path'); +jest.mock('../github'); + +const mockedGetCwd = getCwd as jest.MockedFunction; +const mockedFindup = findup as jest.MockedFunction; +const mockedFsExistsSync = fs.existsSync as jest.MockedFunction< + typeof fs.existsSync +>; +const mockedFsReadFileSync = fs.readFileSync as jest.MockedFunction< + typeof fs.readFileSync +>; + +describe('functions', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createFunction', () => { + const mockFunctionInfo = { + functionsFolder: 'testFolder', + filename: 'testFunction', + endpointPath: '/api/test', + endpointMethod: 'GET', + }; + const mockDest = '/mock/dest'; + + beforeEach(() => { + mockedGetCwd.mockReturnValue('/mock/cwd'); + mockedFindup.mockReturnValue(null); + + // Set up fs.existsSync to return different values for different paths + mockedFsExistsSync.mockImplementation((path: PathLike) => { + if (path === '/mock/dest/testFolder.functions/testFunction.js') { + return false; + } + if (path === '/mock/dest/testFolder.functions/serverless.json') { + return true; + } + if (path === '/mock/dest/testFolder.functions') { + return true; + } + return false; + }); + + // Mock fs.readFileSync to return a valid JSON for the config file + mockedFsReadFileSync.mockReturnValue( + JSON.stringify({ + endpoints: {}, + }) + ); + }); + + it('should create a new function successfully', async () => { + await createFunction(mockFunctionInfo, mockDest); + + expect(fs.mkdirp).not.toHaveBeenCalled(); + + expect(downloadGithubRepoContents).toHaveBeenCalledWith( + 'HubSpot/cms-sample-assets', + 'functions/sample-function.js', + '/mock/dest/testFolder.functions/testFunction.js' + ); + + // Check that the config file was updated + expect(fs.writeFileSync).toHaveBeenCalledWith( + '/mock/dest/testFolder.functions/serverless.json', + expect.any(String) + ); + + // Verify that the logger was called with the correct messages + jest.spyOn(logger, 'log').mockImplementation(() => { + 'The /mock/dest/testFolder.functions path already exists'; + }); + jest.spyOn(logger, 'log').mockImplementation(() => { + 'Created /mock/dest/testFolder.functions/testFunction.js'; + }); + }); + }); + + describe('isObjectOrFunction', () => { + it('should return true for objects', () => { + expect(isObjectOrFunction({})).toBe(true); + }); + + it('should return true for functions', () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + expect(isObjectOrFunction(() => {})).toBe(true); + }); + + it('should return false for null', () => { + // @ts-expect-error test case + expect(isObjectOrFunction(null)).toBe(false); + }); + + it('should return false for primitives', () => { + // @ts-expect-error test case + expect(isObjectOrFunction(5)).toBe(false); + // @ts-expect-error test case + expect(isObjectOrFunction('string')).toBe(false); + // @ts-expect-error test case + expect(isObjectOrFunction(true)).toBe(false); + }); + }); + + describe('createEndpoint', () => { + it('should create an endpoint object', () => { + const result = createEndpoint('POST', 'test.js'); + expect(result).toEqual({ + method: 'POST', + file: 'test.js', + }); + }); + + it('should default to GET method if not provided', () => { + const result = createEndpoint('', 'test.js'); + expect(result).toEqual({ + method: 'GET', + file: 'test.js', + }); + }); + }); + + describe('createConfig', () => { + it('should create a config object', () => { + const result = createConfig({ + endpointPath: '/api/test', + endpointMethod: 'POST', + functionFile: 'test.js', + }); + + expect(result).toEqual({ + runtime: 'nodejs18.x', + version: '1.0', + environment: {}, + secrets: [], + endpoints: { + '/api/test': { + method: 'POST', + file: 'test.js', + }, + }, + }); + }); + }); +}); diff --git a/lib/cms/functions.ts b/lib/cms/functions.ts index 8856465..7e08d8a 100644 --- a/lib/cms/functions.ts +++ b/lib/cms/functions.ts @@ -16,12 +16,12 @@ import { } from '../../types/Functions'; const i18nKey = 'lib.cms.functions'; -function isObjectOrFunction(value: object): boolean { +export function isObjectOrFunction(value: object): boolean { const type = typeof value; return value != null && (type === 'object' || type === 'function'); } -function createEndpoint( +export function createEndpoint( endpointMethod: string, filename: string ): { method: string; file: string } { @@ -31,7 +31,7 @@ function createEndpoint( }; } -function createConfig({ +export function createConfig({ endpointPath, endpointMethod, functionFile, From 2f6fa58d02946f7e60809583ffec15c54f1febf2 Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Tue, 24 Sep 2024 14:33:07 -0400 Subject: [PATCH 2/9] Add cms/themes tests --- lib/__tests__/themes.test.ts | 84 ++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 lib/__tests__/themes.test.ts diff --git a/lib/__tests__/themes.test.ts b/lib/__tests__/themes.test.ts new file mode 100644 index 0000000..9ad3a38 --- /dev/null +++ b/lib/__tests__/themes.test.ts @@ -0,0 +1,84 @@ +import findup from 'findup-sync'; +import { getHubSpotWebsiteOrigin } from '../urls'; +import { getThemeJSONPath, getThemePreviewUrl } from '../cms/themes'; // Adjust the path to your module +import { getEnv } from '../../config'; +import { ENVIRONMENTS } from '../../constants/environments'; + +jest.mock('findup-sync'); +jest.mock('../urls'); +jest.mock('../../config'); +jest.mock('../../constants/environments', () => ({ + ENVIRONMENTS: { + PROD: 'https://prod.hubspot.com', + QA: 'https://qa.hubspot.com', + }, +})); + +const mockedFindup = findup as jest.MockedFunction; +const mockedGetEnv = getEnv as jest.MockedFunction; +const mockedGetHubSpotWebsiteOrigin = + getHubSpotWebsiteOrigin as jest.MockedFunction< + typeof getHubSpotWebsiteOrigin + >; + +describe('getThemeJSONPath', () => { + it('should return the theme.json path if found', () => { + mockedFindup.mockReturnValue('/path/to/theme.json'); + + const result = getThemeJSONPath('/some/path'); + + expect(findup).toHaveBeenCalledWith('theme.json', { + cwd: '/some/path', + nocase: true, + }); + expect(result).toBe('/path/to/theme.json'); + }); + + it('should return null if theme.json is not found', () => { + mockedFindup.mockReturnValue(null); + + const result = getThemeJSONPath('/some/path'); + + expect(findup).toHaveBeenCalledWith('theme.json', { + cwd: '/some/path', + nocase: true, + }); + expect(result).toBeNull(); + }); +}); + +describe('getThemePreviewUrl', () => { + it('should return the correct theme preview URL for PROD environment', () => { + mockedFindup.mockReturnValue('/path/to/theme.json'); + mockedGetEnv.mockReturnValue('prod'); + mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://prod.hubspot.com'); + + const result = getThemePreviewUrl('/path/to/file', 12345); + + expect(getEnv).toHaveBeenCalledWith(12345); + expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.PROD); + expect(result).toBe( + 'https://prod.hubspot.com/theme-previewer/12345/edit/to' + ); + }); + + it('should return the correct theme preview URL for QA environment', () => { + mockedFindup.mockReturnValue('/path/to/theme.json'); + mockedGetEnv.mockReturnValue('qa'); + mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://qa.hubspot.com'); + + const result = getThemePreviewUrl('/path/to/file', 12345); + + expect(getEnv).toHaveBeenCalledWith(12345); + expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.QA); + expect(result).toBe('https://qa.hubspot.com/theme-previewer/12345/edit/to'); + }); + + it('should return undefined if theme.json is not found', () => { + mockedFindup.mockReturnValue(null); + + const result = getThemePreviewUrl('/invalid/path', 12345); + + expect(result).toBeUndefined(); + }); +}); From 2af52b29c2954c7730e0accb72c884a490db66ab Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Wed, 25 Sep 2024 12:08:56 -0400 Subject: [PATCH 3/9] Add validate tests --- lib/__tests__/validate.test.ts | 112 +++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 lib/__tests__/validate.test.ts diff --git a/lib/__tests__/validate.test.ts b/lib/__tests__/validate.test.ts new file mode 100644 index 0000000..fd34032 --- /dev/null +++ b/lib/__tests__/validate.test.ts @@ -0,0 +1,112 @@ +import fs, { Stats } from 'fs-extra'; +import { validateHubl } from '../../api/validateHubl'; +import { walk } from '../fs'; +import { lint } from '../cms/validate'; +import { LintResult, Validation } from '../../types/HublValidation'; + +jest.mock('fs-extra'); +jest.mock('../../api/validateHubl'); +jest.mock('../fs'); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockedFsStat = fs.stat as jest.MockedFunction; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockedFsReadFile = fs.readFile as jest.MockedFunction; +const mockedWalk = walk as jest.MockedFunction; +const mockedValidateHubl = validateHubl as jest.MockedFunction< + typeof validateHubl +>; + +const mockFsStats = jest.createMockFromModule('fs-extra'); + +mockFsStats.isDirectory = jest.fn(() => true); + +const mockValidation: Validation = { + meta: { + all_widgets: [], + widgets_in_rich_text: [], + editable_flex_areas: [], + editable_layout_sections: null, + email_style_settings: null, + sms_flex_area: [], + google_font_variations: null, + custom_font_variations: [], + has_style_tag: false, + has_header_tag: false, + output_html: '', + has_menu_tag: false, + has_theme_setting_function: false, + template_source: '', + attribute_defaults: null, + template_errors: [], + path_dependencies: [], + theme_field_dependencies: [], + template_type_ids: null, + exact_path_references: [], + required_scopes_to_render: [], + }, + renderingErrors: [], + html: '', +}; + +describe('lint function', () => { + const accountId = 123; + const filePath = 'test.html'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return an empty array if directory has no files', async () => { + mockedFsStat.mockResolvedValue(mockFsStats); + mockedWalk.mockResolvedValue([]); + + const result = await lint(accountId, filePath); + expect(result).toEqual([]); + }); + + it('should return the correct object if a file has no content', async () => { + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(' '); + + const result = await lint(accountId, filePath); + expect(result).toEqual([{ file: filePath, validation: null }]); + }); + + it('should call validateHubl with the correct parameters', async () => { + const mockSource = 'valid HUBL content'; + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(mockSource); + mockedValidateHubl.mockResolvedValue(mockValidation); + const result = await lint(accountId, filePath); + expect(validateHubl).toHaveBeenCalledWith(accountId, mockSource); + expect(result).toEqual([{ file: filePath, validation: mockValidation }]); + }); + + it('should filter out files with invalid extensions', async () => { + const invalidFile = 'test.txt'; + mockedFsStat.mockResolvedValue({ isDirectory: () => true }); + mockedWalk.mockResolvedValue([invalidFile, filePath]); + mockedFsReadFile.mockResolvedValue('valid HUBL content'); + mockedValidateHubl.mockResolvedValue(mockValidation); + + const result = await lint(accountId, filePath); + + expect(result).toHaveLength(1); + expect((result as Partial[])[0].file).toBe(filePath); + }); + + it('should execute callback if provided', async () => { + const mockCallback = jest.fn(); + const mockSource = 'valid HUBL content'; + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(mockSource); + mockedValidateHubl.mockResolvedValue(mockValidation); + + await lint(accountId, filePath, mockCallback); + expect(mockCallback).toHaveBeenCalledWith({ + file: filePath, + validation: mockValidation, + }); + }); +}); From cc9e68fdcd607b9e2913af4d329171094e97dfed Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Wed, 25 Sep 2024 14:58:32 -0400 Subject: [PATCH 4/9] Add cms watch tests --- lib/__tests__/watch.test.ts | 191 ++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 lib/__tests__/watch.test.ts diff --git a/lib/__tests__/watch.test.ts b/lib/__tests__/watch.test.ts new file mode 100644 index 0000000..1900ec5 --- /dev/null +++ b/lib/__tests__/watch.test.ts @@ -0,0 +1,191 @@ +import chokidar from 'chokidar'; +import PQueue from 'p-queue'; + +import { uploadFolder } from '../cms/uploadFolder'; +import { logger } from '../logger'; +import { watch } from '../cms/watch'; +import { MODE } from '../../constants/files'; + +jest.mock('chokidar'); +jest.mock('axios'); +jest.mock('p-queue'); +jest.mock('../cms/uploadFolder'); + +describe('watch function', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let chokidarMock: any; + let pQueueAddMock: jest.Mock; + + beforeEach(() => { + chokidarMock = { + watch: jest.fn().mockReturnThis(), + on: jest.fn().mockReturnThis(), + }; + (chokidar.watch as jest.Mock).mockReturnValue(chokidarMock); + + pQueueAddMock = jest.fn(); + + // @ts-expect-error test case + PQueue.mockImplementation(() => ({ + add: pQueueAddMock, + size: 0, + })); + logger.log = jest.fn(); + logger.debug = jest.fn(); + }); + + it('should call chokidar.watch with correct arguments', () => { + const accountId = 123; + const src = 'src-folder'; + const dest = 'dest-folder'; + const options = { + mode: MODE.draft, + remove: false, + disableInitial: true, + notify: '', + commandOptions: {}, + filePaths: [], + }; + + watch(accountId, src, dest, options); + + expect(chokidar.watch).toHaveBeenCalledWith(src, { + ignoreInitial: true, + ignored: expect.any(Function), + }); + }); + + it('should trigger folder upload on initialization', async () => { + const accountId = 123; + const src = 'src-folder'; + const dest = 'dest-folder'; + const options = { + mode: MODE.draft, + remove: false, + disableInitial: false, + notify: '', + commandOptions: {}, + filePaths: [], + }; + const postInitialUploadCallback = jest.fn(); + + (uploadFolder as jest.Mock).mockResolvedValueOnce([]); + + await watch(accountId, src, dest, options, postInitialUploadCallback); + + expect(uploadFolder).toHaveBeenCalledWith( + accountId, + src, + dest, + {}, + options.commandOptions, + options.filePaths, + options.mode + ); + expect(postInitialUploadCallback).toHaveBeenCalled(); + }); + + it('should log ready when watcher is ready', () => { + const accountId = 123; + const src = 'src-folder'; + const dest = 'dest-folder'; + const options = { + mode: MODE.draft, + remove: false, + disableInitial: true, + notify: '', + commandOptions: {}, + filePaths: [], + }; + + watch(accountId, src, dest, options); + + expect(chokidarMock.on).toHaveBeenCalledWith('ready', expect.any(Function)); + chokidarMock.on.mock.calls[0][1](); + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('ready')); + }); + + it('should upload file when file is added', () => { + const accountId = 123; + const src = 'src-folder'; + const dest = 'dest-folder'; + const options = { + mode: MODE.draft, + remove: false, + disableInitial: true, + notify: '', + commandOptions: {}, + filePaths: [], + }; + + watch(accountId, src, dest, options); + + expect(chokidarMock.on).toHaveBeenCalledWith('add', expect.any(Function)); + const addCallback = chokidarMock.on.mock.calls[1][1]; + const filePath = '/some-file-path.html'; + + addCallback(filePath); + + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('Attempting to upload') + ); + }); + + it('should handle file change event and upload file', () => { + const accountId = 123; + const src = 'src-folder'; + const dest = 'dest-folder'; + const options = { + mode: MODE.draft, + remove: false, + disableInitial: true, + notify: '', + commandOptions: {}, + filePaths: [], + }; + + watch(accountId, src, dest, options); + + expect(chokidarMock.on).toHaveBeenCalledWith( + 'change', + expect.any(Function) + ); + const changeCallback = chokidarMock.on.mock.calls[2][1]; + const filePath = 'changed-file-path.html'; + + changeCallback(filePath); + + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('Attempting to upload') + ); + }); + + it('should handle file delete event', () => { + const accountId = 123; + const src = 'src-folder'; + const dest = 'dest-folder'; + const options = { + mode: MODE.draft, + remove: true, + disableInitial: true, + notify: '', + commandOptions: {}, + filePaths: [], + }; + + watch(accountId, src, dest, options); + + expect(chokidarMock.on).toHaveBeenCalledWith( + 'unlink', + expect.any(Function) + ); + const deleteCallback = chokidarMock.on.mock.calls[2][1]; + const filePath = 'deleted-file-path.html'; + + deleteCallback(filePath); + + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('Attempting to delete') + ); + }); +}); From e98326c0500fa5716cd9765c2558f402dabeec3d Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Tue, 1 Oct 2024 11:05:58 -0400 Subject: [PATCH 5/9] Rename tests & address feedback --- lib/__tests__/functions.test.ts | 15 +----- lib/__tests__/themes.test.ts | 90 ++++++++++++++++--------------- lib/__tests__/validate.test.ts | 96 ++++++++++++++++----------------- lib/__tests__/watch.test.ts | 2 +- 4 files changed, 96 insertions(+), 107 deletions(-) diff --git a/lib/__tests__/functions.test.ts b/lib/__tests__/functions.test.ts index 887a26c..0f95053 100644 --- a/lib/__tests__/functions.test.ts +++ b/lib/__tests__/functions.test.ts @@ -2,7 +2,6 @@ import fs, { PathLike } from 'fs-extra'; import findup from 'findup-sync'; import { getCwd } from '../path'; import { downloadGithubRepoContents } from '../github'; -import { logger } from '../logger'; import { createFunction, isObjectOrFunction, @@ -24,11 +23,7 @@ const mockedFsReadFileSync = fs.readFileSync as jest.MockedFunction< typeof fs.readFileSync >; -describe('functions', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - +describe('lib/cms/functions', () => { describe('createFunction', () => { const mockFunctionInfo = { functionsFolder: 'testFolder', @@ -80,14 +75,6 @@ describe('functions', () => { '/mock/dest/testFolder.functions/serverless.json', expect.any(String) ); - - // Verify that the logger was called with the correct messages - jest.spyOn(logger, 'log').mockImplementation(() => { - 'The /mock/dest/testFolder.functions path already exists'; - }); - jest.spyOn(logger, 'log').mockImplementation(() => { - 'Created /mock/dest/testFolder.functions/testFunction.js'; - }); }); }); diff --git a/lib/__tests__/themes.test.ts b/lib/__tests__/themes.test.ts index 9ad3a38..391d536 100644 --- a/lib/__tests__/themes.test.ts +++ b/lib/__tests__/themes.test.ts @@ -1,6 +1,6 @@ import findup from 'findup-sync'; import { getHubSpotWebsiteOrigin } from '../urls'; -import { getThemeJSONPath, getThemePreviewUrl } from '../cms/themes'; // Adjust the path to your module +import { getThemeJSONPath, getThemePreviewUrl } from '../cms/themes'; import { getEnv } from '../../config'; import { ENVIRONMENTS } from '../../constants/environments'; @@ -21,64 +21,68 @@ const mockedGetHubSpotWebsiteOrigin = typeof getHubSpotWebsiteOrigin >; -describe('getThemeJSONPath', () => { - it('should return the theme.json path if found', () => { - mockedFindup.mockReturnValue('/path/to/theme.json'); +describe('lib/cms/themes', () => { + describe('getThemeJSONPath', () => { + it('should return the theme.json path if found', () => { + mockedFindup.mockReturnValue('/path/to/theme.json'); - const result = getThemeJSONPath('/some/path'); + const result = getThemeJSONPath('/some/path'); - expect(findup).toHaveBeenCalledWith('theme.json', { - cwd: '/some/path', - nocase: true, + expect(findup).toHaveBeenCalledWith('theme.json', { + cwd: '/some/path', + nocase: true, + }); + expect(result).toBe('/path/to/theme.json'); }); - expect(result).toBe('/path/to/theme.json'); - }); - it('should return null if theme.json is not found', () => { - mockedFindup.mockReturnValue(null); + it('should return null if theme.json is not found', () => { + mockedFindup.mockReturnValue(null); - const result = getThemeJSONPath('/some/path'); + const result = getThemeJSONPath('/some/path'); - expect(findup).toHaveBeenCalledWith('theme.json', { - cwd: '/some/path', - nocase: true, + expect(findup).toHaveBeenCalledWith('theme.json', { + cwd: '/some/path', + nocase: true, + }); + expect(result).toBeNull(); }); - expect(result).toBeNull(); }); -}); -describe('getThemePreviewUrl', () => { - it('should return the correct theme preview URL for PROD environment', () => { - mockedFindup.mockReturnValue('/path/to/theme.json'); - mockedGetEnv.mockReturnValue('prod'); - mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://prod.hubspot.com'); + describe('getThemePreviewUrl', () => { + it('should return the correct theme preview URL for PROD environment', () => { + mockedFindup.mockReturnValue('/src/my-theme/theme.json'); + mockedGetEnv.mockReturnValue('prod'); + mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://prod.hubspot.com'); - const result = getThemePreviewUrl('/path/to/file', 12345); + const result = getThemePreviewUrl('/path/to/file', 12345); - expect(getEnv).toHaveBeenCalledWith(12345); - expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.PROD); - expect(result).toBe( - 'https://prod.hubspot.com/theme-previewer/12345/edit/to' - ); - }); + expect(getEnv).toHaveBeenCalledWith(12345); + expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.PROD); + expect(result).toBe( + 'https://prod.hubspot.com/theme-previewer/12345/edit/my-theme' + ); + }); - it('should return the correct theme preview URL for QA environment', () => { - mockedFindup.mockReturnValue('/path/to/theme.json'); - mockedGetEnv.mockReturnValue('qa'); - mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://qa.hubspot.com'); + it('should return the correct theme preview URL for QA environment', () => { + mockedFindup.mockReturnValue('/src/my-theme/theme.json'); + mockedGetEnv.mockReturnValue('qa'); + mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://qa.hubspot.com'); - const result = getThemePreviewUrl('/path/to/file', 12345); + const result = getThemePreviewUrl('/path/to/file', 12345); - expect(getEnv).toHaveBeenCalledWith(12345); - expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.QA); - expect(result).toBe('https://qa.hubspot.com/theme-previewer/12345/edit/to'); - }); + expect(getEnv).toHaveBeenCalledWith(12345); + expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.QA); + expect(result).toBe( + 'https://qa.hubspot.com/theme-previewer/12345/edit/my-theme' + ); + }); - it('should return undefined if theme.json is not found', () => { - mockedFindup.mockReturnValue(null); + it('should return undefined if theme.json is not found', () => { + mockedFindup.mockReturnValue(null); - const result = getThemePreviewUrl('/invalid/path', 12345); + const result = getThemePreviewUrl('/invalid/path', 12345); - expect(result).toBeUndefined(); + expect(result).toBeUndefined(); + }); }); }); diff --git a/lib/__tests__/validate.test.ts b/lib/__tests__/validate.test.ts index fd34032..d829533 100644 --- a/lib/__tests__/validate.test.ts +++ b/lib/__tests__/validate.test.ts @@ -49,64 +49,62 @@ const mockValidation: Validation = { html: '', }; -describe('lint function', () => { - const accountId = 123; - const filePath = 'test.html'; +describe('lib/cms/validate', () => { + describe('lint function', () => { + const accountId = 123; + const filePath = 'test.html'; - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should return an empty array if directory has no files', async () => { - mockedFsStat.mockResolvedValue(mockFsStats); - mockedWalk.mockResolvedValue([]); + it('should return an empty array if directory has no files', async () => { + mockedFsStat.mockResolvedValue(mockFsStats); + mockedWalk.mockResolvedValue([]); - const result = await lint(accountId, filePath); - expect(result).toEqual([]); - }); + const result = await lint(accountId, filePath); + expect(result).toEqual([]); + }); - it('should return the correct object if a file has no content', async () => { - mockedFsStat.mockResolvedValue({ isDirectory: () => false }); - mockedFsReadFile.mockResolvedValue(' '); + it('should return the correct object if a file has no content', async () => { + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(' '); - const result = await lint(accountId, filePath); - expect(result).toEqual([{ file: filePath, validation: null }]); - }); + const result = await lint(accountId, filePath); + expect(result).toEqual([{ file: filePath, validation: null }]); + }); - it('should call validateHubl with the correct parameters', async () => { - const mockSource = 'valid HUBL content'; - mockedFsStat.mockResolvedValue({ isDirectory: () => false }); - mockedFsReadFile.mockResolvedValue(mockSource); - mockedValidateHubl.mockResolvedValue(mockValidation); - const result = await lint(accountId, filePath); - expect(validateHubl).toHaveBeenCalledWith(accountId, mockSource); - expect(result).toEqual([{ file: filePath, validation: mockValidation }]); - }); + it('should call validateHubl with the correct parameters', async () => { + const mockSource = 'valid HUBL content'; + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(mockSource); + mockedValidateHubl.mockResolvedValue(mockValidation); + const result = await lint(accountId, filePath); + expect(validateHubl).toHaveBeenCalledWith(accountId, mockSource); + expect(result).toEqual([{ file: filePath, validation: mockValidation }]); + }); - it('should filter out files with invalid extensions', async () => { - const invalidFile = 'test.txt'; - mockedFsStat.mockResolvedValue({ isDirectory: () => true }); - mockedWalk.mockResolvedValue([invalidFile, filePath]); - mockedFsReadFile.mockResolvedValue('valid HUBL content'); - mockedValidateHubl.mockResolvedValue(mockValidation); + it('should filter out files with invalid extensions', async () => { + const invalidFile = 'test.txt'; + mockedFsStat.mockResolvedValue({ isDirectory: () => true }); + mockedWalk.mockResolvedValue([invalidFile, filePath]); + mockedFsReadFile.mockResolvedValue('valid HUBL content'); + mockedValidateHubl.mockResolvedValue(mockValidation); - const result = await lint(accountId, filePath); + const result = await lint(accountId, filePath); - expect(result).toHaveLength(1); - expect((result as Partial[])[0].file).toBe(filePath); - }); + expect(result).toHaveLength(1); + expect((result as Partial[])[0].file).toBe(filePath); + }); - it('should execute callback if provided', async () => { - const mockCallback = jest.fn(); - const mockSource = 'valid HUBL content'; - mockedFsStat.mockResolvedValue({ isDirectory: () => false }); - mockedFsReadFile.mockResolvedValue(mockSource); - mockedValidateHubl.mockResolvedValue(mockValidation); - - await lint(accountId, filePath, mockCallback); - expect(mockCallback).toHaveBeenCalledWith({ - file: filePath, - validation: mockValidation, + it('should execute callback if provided', async () => { + const mockCallback = jest.fn(); + const mockSource = 'valid HUBL content'; + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(mockSource); + mockedValidateHubl.mockResolvedValue(mockValidation); + + await lint(accountId, filePath, mockCallback); + expect(mockCallback).toHaveBeenCalledWith({ + file: filePath, + validation: mockValidation, + }); }); }); }); diff --git a/lib/__tests__/watch.test.ts b/lib/__tests__/watch.test.ts index 1900ec5..785b932 100644 --- a/lib/__tests__/watch.test.ts +++ b/lib/__tests__/watch.test.ts @@ -11,7 +11,7 @@ jest.mock('axios'); jest.mock('p-queue'); jest.mock('../cms/uploadFolder'); -describe('watch function', () => { +describe('lib/cms/watch', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let chokidarMock: any; let pQueueAddMock: jest.Mock; From a54efdd0207005059c9f34db23cfaa33e8dc5de5 Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Tue, 1 Oct 2024 11:46:11 -0400 Subject: [PATCH 6/9] Address feedback p2 --- lib/__tests__/watch.test.ts | 37 +++---------------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/lib/__tests__/watch.test.ts b/lib/__tests__/watch.test.ts index 785b932..000ed04 100644 --- a/lib/__tests__/watch.test.ts +++ b/lib/__tests__/watch.test.ts @@ -1,8 +1,8 @@ import chokidar from 'chokidar'; import PQueue from 'p-queue'; +import fs from 'fs'; import { uploadFolder } from '../cms/uploadFolder'; -import { logger } from '../logger'; import { watch } from '../cms/watch'; import { MODE } from '../../constants/files'; @@ -10,6 +10,7 @@ jest.mock('chokidar'); jest.mock('axios'); jest.mock('p-queue'); jest.mock('../cms/uploadFolder'); +jest.mock('fs'); describe('lib/cms/watch', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -30,8 +31,6 @@ describe('lib/cms/watch', () => { add: pQueueAddMock, size: 0, })); - logger.log = jest.fn(); - logger.debug = jest.fn(); }); it('should call chokidar.watch with correct arguments', () => { @@ -85,26 +84,6 @@ describe('lib/cms/watch', () => { expect(postInitialUploadCallback).toHaveBeenCalled(); }); - it('should log ready when watcher is ready', () => { - const accountId = 123; - const src = 'src-folder'; - const dest = 'dest-folder'; - const options = { - mode: MODE.draft, - remove: false, - disableInitial: true, - notify: '', - commandOptions: {}, - filePaths: [], - }; - - watch(accountId, src, dest, options); - - expect(chokidarMock.on).toHaveBeenCalledWith('ready', expect.any(Function)); - chokidarMock.on.mock.calls[0][1](); - expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('ready')); - }); - it('should upload file when file is added', () => { const accountId = 123; const src = 'src-folder'; @@ -125,10 +104,6 @@ describe('lib/cms/watch', () => { const filePath = '/some-file-path.html'; addCallback(filePath); - - expect(logger.debug).toHaveBeenCalledWith( - expect.stringContaining('Attempting to upload') - ); }); it('should handle file change event and upload file', () => { @@ -154,10 +129,6 @@ describe('lib/cms/watch', () => { const filePath = 'changed-file-path.html'; changeCallback(filePath); - - expect(logger.debug).toHaveBeenCalledWith( - expect.stringContaining('Attempting to upload') - ); }); it('should handle file delete event', () => { @@ -184,8 +155,6 @@ describe('lib/cms/watch', () => { deleteCallback(filePath); - expect(logger.debug).toHaveBeenCalledWith( - expect.stringContaining('Attempting to delete') - ); + fs.unlinkSync(filePath); }); }); From ffe97a226ee7dccb97dc91352f506dae07a852ae Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Tue, 1 Oct 2024 12:02:25 -0400 Subject: [PATCH 7/9] Unnest validate test --- lib/__tests__/validate.test.ts | 90 +++++++++++++++++----------------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/lib/__tests__/validate.test.ts b/lib/__tests__/validate.test.ts index d829533..214bac9 100644 --- a/lib/__tests__/validate.test.ts +++ b/lib/__tests__/validate.test.ts @@ -50,61 +50,59 @@ const mockValidation: Validation = { }; describe('lib/cms/validate', () => { - describe('lint function', () => { - const accountId = 123; - const filePath = 'test.html'; + const accountId = 123; + const filePath = 'test.html'; - it('should return an empty array if directory has no files', async () => { - mockedFsStat.mockResolvedValue(mockFsStats); - mockedWalk.mockResolvedValue([]); + it('should return an empty array if directory has no files', async () => { + mockedFsStat.mockResolvedValue(mockFsStats); + mockedWalk.mockResolvedValue([]); - const result = await lint(accountId, filePath); - expect(result).toEqual([]); - }); + const result = await lint(accountId, filePath); + expect(result).toEqual([]); + }); - it('should return the correct object if a file has no content', async () => { - mockedFsStat.mockResolvedValue({ isDirectory: () => false }); - mockedFsReadFile.mockResolvedValue(' '); + it('should return the correct object if a file has no content', async () => { + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(' '); - const result = await lint(accountId, filePath); - expect(result).toEqual([{ file: filePath, validation: null }]); - }); + const result = await lint(accountId, filePath); + expect(result).toEqual([{ file: filePath, validation: null }]); + }); - it('should call validateHubl with the correct parameters', async () => { - const mockSource = 'valid HUBL content'; - mockedFsStat.mockResolvedValue({ isDirectory: () => false }); - mockedFsReadFile.mockResolvedValue(mockSource); - mockedValidateHubl.mockResolvedValue(mockValidation); - const result = await lint(accountId, filePath); - expect(validateHubl).toHaveBeenCalledWith(accountId, mockSource); - expect(result).toEqual([{ file: filePath, validation: mockValidation }]); - }); + it('should call validateHubl with the correct parameters', async () => { + const mockSource = 'valid HUBL content'; + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(mockSource); + mockedValidateHubl.mockResolvedValue(mockValidation); + const result = await lint(accountId, filePath); + expect(validateHubl).toHaveBeenCalledWith(accountId, mockSource); + expect(result).toEqual([{ file: filePath, validation: mockValidation }]); + }); - it('should filter out files with invalid extensions', async () => { - const invalidFile = 'test.txt'; - mockedFsStat.mockResolvedValue({ isDirectory: () => true }); - mockedWalk.mockResolvedValue([invalidFile, filePath]); - mockedFsReadFile.mockResolvedValue('valid HUBL content'); - mockedValidateHubl.mockResolvedValue(mockValidation); + it('should filter out files with invalid extensions', async () => { + const invalidFile = 'test.txt'; + mockedFsStat.mockResolvedValue({ isDirectory: () => true }); + mockedWalk.mockResolvedValue([invalidFile, filePath]); + mockedFsReadFile.mockResolvedValue('valid HUBL content'); + mockedValidateHubl.mockResolvedValue(mockValidation); - const result = await lint(accountId, filePath); + const result = await lint(accountId, filePath); - expect(result).toHaveLength(1); - expect((result as Partial[])[0].file).toBe(filePath); - }); + expect(result).toHaveLength(1); + expect((result as Partial[])[0].file).toBe(filePath); + }); - it('should execute callback if provided', async () => { - const mockCallback = jest.fn(); - const mockSource = 'valid HUBL content'; - mockedFsStat.mockResolvedValue({ isDirectory: () => false }); - mockedFsReadFile.mockResolvedValue(mockSource); - mockedValidateHubl.mockResolvedValue(mockValidation); - - await lint(accountId, filePath, mockCallback); - expect(mockCallback).toHaveBeenCalledWith({ - file: filePath, - validation: mockValidation, - }); + it('should execute callback if provided', async () => { + const mockCallback = jest.fn(); + const mockSource = 'valid HUBL content'; + mockedFsStat.mockResolvedValue({ isDirectory: () => false }); + mockedFsReadFile.mockResolvedValue(mockSource); + mockedValidateHubl.mockResolvedValue(mockValidation); + + await lint(accountId, filePath, mockCallback); + expect(mockCallback).toHaveBeenCalledWith({ + file: filePath, + validation: mockValidation, }); }); }); From 6096d8f535f5648d0e4c162aa76e0751d54f1ba2 Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Tue, 1 Oct 2024 15:07:19 -0400 Subject: [PATCH 8/9] Correct TS types --- lib/__tests__/validate.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/__tests__/validate.test.ts b/lib/__tests__/validate.test.ts index 214bac9..7cd9816 100644 --- a/lib/__tests__/validate.test.ts +++ b/lib/__tests__/validate.test.ts @@ -3,6 +3,7 @@ import { validateHubl } from '../../api/validateHubl'; import { walk } from '../fs'; import { lint } from '../cms/validate'; import { LintResult, Validation } from '../../types/HublValidation'; +import { AxiosPromise } from 'axios'; jest.mock('fs-extra'); jest.mock('../../api/validateHubl'); @@ -73,7 +74,9 @@ describe('lib/cms/validate', () => { const mockSource = 'valid HUBL content'; mockedFsStat.mockResolvedValue({ isDirectory: () => false }); mockedFsReadFile.mockResolvedValue(mockSource); - mockedValidateHubl.mockResolvedValue(mockValidation); + mockedValidateHubl.mockResolvedValue({ + data: mockValidation, + } as unknown as AxiosPromise); const result = await lint(accountId, filePath); expect(validateHubl).toHaveBeenCalledWith(accountId, mockSource); expect(result).toEqual([{ file: filePath, validation: mockValidation }]); @@ -84,7 +87,9 @@ describe('lib/cms/validate', () => { mockedFsStat.mockResolvedValue({ isDirectory: () => true }); mockedWalk.mockResolvedValue([invalidFile, filePath]); mockedFsReadFile.mockResolvedValue('valid HUBL content'); - mockedValidateHubl.mockResolvedValue(mockValidation); + mockedValidateHubl.mockResolvedValue({ + data: mockValidation, + } as unknown as AxiosPromise); const result = await lint(accountId, filePath); @@ -97,7 +102,9 @@ describe('lib/cms/validate', () => { const mockSource = 'valid HUBL content'; mockedFsStat.mockResolvedValue({ isDirectory: () => false }); mockedFsReadFile.mockResolvedValue(mockSource); - mockedValidateHubl.mockResolvedValue(mockValidation); + mockedValidateHubl.mockResolvedValue({ + data: mockValidation, + } as unknown as AxiosPromise); await lint(accountId, filePath, mockCallback); expect(mockCallback).toHaveBeenCalledWith({ From ac17f42e7f897dc212aeeb5a20dcec146a9df08a Mon Sep 17 00:00:00 2001 From: Allison Kemmerle Date: Tue, 1 Oct 2024 15:54:22 -0400 Subject: [PATCH 9/9] Address feedback p3 --- lib/__tests__/watch.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/__tests__/watch.test.ts b/lib/__tests__/watch.test.ts index 000ed04..c0f23c5 100644 --- a/lib/__tests__/watch.test.ts +++ b/lib/__tests__/watch.test.ts @@ -1,6 +1,5 @@ import chokidar from 'chokidar'; import PQueue from 'p-queue'; -import fs from 'fs'; import { uploadFolder } from '../cms/uploadFolder'; import { watch } from '../cms/watch'; @@ -10,7 +9,6 @@ jest.mock('chokidar'); jest.mock('axios'); jest.mock('p-queue'); jest.mock('../cms/uploadFolder'); -jest.mock('fs'); describe('lib/cms/watch', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -100,10 +98,6 @@ describe('lib/cms/watch', () => { watch(accountId, src, dest, options); expect(chokidarMock.on).toHaveBeenCalledWith('add', expect.any(Function)); - const addCallback = chokidarMock.on.mock.calls[1][1]; - const filePath = '/some-file-path.html'; - - addCallback(filePath); }); it('should handle file change event and upload file', () => { @@ -125,10 +119,6 @@ describe('lib/cms/watch', () => { 'change', expect.any(Function) ); - const changeCallback = chokidarMock.on.mock.calls[2][1]; - const filePath = 'changed-file-path.html'; - - changeCallback(filePath); }); it('should handle file delete event', () => { @@ -150,11 +140,5 @@ describe('lib/cms/watch', () => { 'unlink', expect.any(Function) ); - const deleteCallback = chokidarMock.on.mock.calls[2][1]; - const filePath = 'deleted-file-path.html'; - - deleteCallback(filePath); - - fs.unlinkSync(filePath); }); });