Skip to content

Commit f5e28c8

Browse files
authored
Merge pull request #195 from HubSpot/add/cms-tests
chore: Add tests for CMS utils
2 parents 2e20b69 + ac17f42 commit f5e28c8

File tree

5 files changed

+496
-3
lines changed

5 files changed

+496
-3
lines changed

lib/__tests__/functions.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import fs, { PathLike } from 'fs-extra';
2+
import findup from 'findup-sync';
3+
import { getCwd } from '../path';
4+
import { downloadGithubRepoContents } from '../github';
5+
import {
6+
createFunction,
7+
isObjectOrFunction,
8+
createEndpoint,
9+
createConfig,
10+
} from '../cms/functions';
11+
12+
jest.mock('fs-extra');
13+
jest.mock('findup-sync');
14+
jest.mock('../path');
15+
jest.mock('../github');
16+
17+
const mockedGetCwd = getCwd as jest.MockedFunction<typeof getCwd>;
18+
const mockedFindup = findup as jest.MockedFunction<typeof findup>;
19+
const mockedFsExistsSync = fs.existsSync as jest.MockedFunction<
20+
typeof fs.existsSync
21+
>;
22+
const mockedFsReadFileSync = fs.readFileSync as jest.MockedFunction<
23+
typeof fs.readFileSync
24+
>;
25+
26+
describe('lib/cms/functions', () => {
27+
describe('createFunction', () => {
28+
const mockFunctionInfo = {
29+
functionsFolder: 'testFolder',
30+
filename: 'testFunction',
31+
endpointPath: '/api/test',
32+
endpointMethod: 'GET',
33+
};
34+
const mockDest = '/mock/dest';
35+
36+
beforeEach(() => {
37+
mockedGetCwd.mockReturnValue('/mock/cwd');
38+
mockedFindup.mockReturnValue(null);
39+
40+
// Set up fs.existsSync to return different values for different paths
41+
mockedFsExistsSync.mockImplementation((path: PathLike) => {
42+
if (path === '/mock/dest/testFolder.functions/testFunction.js') {
43+
return false;
44+
}
45+
if (path === '/mock/dest/testFolder.functions/serverless.json') {
46+
return true;
47+
}
48+
if (path === '/mock/dest/testFolder.functions') {
49+
return true;
50+
}
51+
return false;
52+
});
53+
54+
// Mock fs.readFileSync to return a valid JSON for the config file
55+
mockedFsReadFileSync.mockReturnValue(
56+
JSON.stringify({
57+
endpoints: {},
58+
})
59+
);
60+
});
61+
62+
it('should create a new function successfully', async () => {
63+
await createFunction(mockFunctionInfo, mockDest);
64+
65+
expect(fs.mkdirp).not.toHaveBeenCalled();
66+
67+
expect(downloadGithubRepoContents).toHaveBeenCalledWith(
68+
'HubSpot/cms-sample-assets',
69+
'functions/sample-function.js',
70+
'/mock/dest/testFolder.functions/testFunction.js'
71+
);
72+
73+
// Check that the config file was updated
74+
expect(fs.writeFileSync).toHaveBeenCalledWith(
75+
'/mock/dest/testFolder.functions/serverless.json',
76+
expect.any(String)
77+
);
78+
});
79+
});
80+
81+
describe('isObjectOrFunction', () => {
82+
it('should return true for objects', () => {
83+
expect(isObjectOrFunction({})).toBe(true);
84+
});
85+
86+
it('should return true for functions', () => {
87+
// eslint-disable-next-line @typescript-eslint/no-empty-function
88+
expect(isObjectOrFunction(() => {})).toBe(true);
89+
});
90+
91+
it('should return false for null', () => {
92+
// @ts-expect-error test case
93+
expect(isObjectOrFunction(null)).toBe(false);
94+
});
95+
96+
it('should return false for primitives', () => {
97+
// @ts-expect-error test case
98+
expect(isObjectOrFunction(5)).toBe(false);
99+
// @ts-expect-error test case
100+
expect(isObjectOrFunction('string')).toBe(false);
101+
// @ts-expect-error test case
102+
expect(isObjectOrFunction(true)).toBe(false);
103+
});
104+
});
105+
106+
describe('createEndpoint', () => {
107+
it('should create an endpoint object', () => {
108+
const result = createEndpoint('POST', 'test.js');
109+
expect(result).toEqual({
110+
method: 'POST',
111+
file: 'test.js',
112+
});
113+
});
114+
115+
it('should default to GET method if not provided', () => {
116+
const result = createEndpoint('', 'test.js');
117+
expect(result).toEqual({
118+
method: 'GET',
119+
file: 'test.js',
120+
});
121+
});
122+
});
123+
124+
describe('createConfig', () => {
125+
it('should create a config object', () => {
126+
const result = createConfig({
127+
endpointPath: '/api/test',
128+
endpointMethod: 'POST',
129+
functionFile: 'test.js',
130+
});
131+
132+
expect(result).toEqual({
133+
runtime: 'nodejs18.x',
134+
version: '1.0',
135+
environment: {},
136+
secrets: [],
137+
endpoints: {
138+
'/api/test': {
139+
method: 'POST',
140+
file: 'test.js',
141+
},
142+
},
143+
});
144+
});
145+
});
146+
});

lib/__tests__/themes.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import findup from 'findup-sync';
2+
import { getHubSpotWebsiteOrigin } from '../urls';
3+
import { getThemeJSONPath, getThemePreviewUrl } from '../cms/themes';
4+
import { getEnv } from '../../config';
5+
import { ENVIRONMENTS } from '../../constants/environments';
6+
7+
jest.mock('findup-sync');
8+
jest.mock('../urls');
9+
jest.mock('../../config');
10+
jest.mock('../../constants/environments', () => ({
11+
ENVIRONMENTS: {
12+
PROD: 'https://prod.hubspot.com',
13+
QA: 'https://qa.hubspot.com',
14+
},
15+
}));
16+
17+
const mockedFindup = findup as jest.MockedFunction<typeof findup>;
18+
const mockedGetEnv = getEnv as jest.MockedFunction<typeof getEnv>;
19+
const mockedGetHubSpotWebsiteOrigin =
20+
getHubSpotWebsiteOrigin as jest.MockedFunction<
21+
typeof getHubSpotWebsiteOrigin
22+
>;
23+
24+
describe('lib/cms/themes', () => {
25+
describe('getThemeJSONPath', () => {
26+
it('should return the theme.json path if found', () => {
27+
mockedFindup.mockReturnValue('/path/to/theme.json');
28+
29+
const result = getThemeJSONPath('/some/path');
30+
31+
expect(findup).toHaveBeenCalledWith('theme.json', {
32+
cwd: '/some/path',
33+
nocase: true,
34+
});
35+
expect(result).toBe('/path/to/theme.json');
36+
});
37+
38+
it('should return null if theme.json is not found', () => {
39+
mockedFindup.mockReturnValue(null);
40+
41+
const result = getThemeJSONPath('/some/path');
42+
43+
expect(findup).toHaveBeenCalledWith('theme.json', {
44+
cwd: '/some/path',
45+
nocase: true,
46+
});
47+
expect(result).toBeNull();
48+
});
49+
});
50+
51+
describe('getThemePreviewUrl', () => {
52+
it('should return the correct theme preview URL for PROD environment', () => {
53+
mockedFindup.mockReturnValue('/src/my-theme/theme.json');
54+
mockedGetEnv.mockReturnValue('prod');
55+
mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://prod.hubspot.com');
56+
57+
const result = getThemePreviewUrl('/path/to/file', 12345);
58+
59+
expect(getEnv).toHaveBeenCalledWith(12345);
60+
expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.PROD);
61+
expect(result).toBe(
62+
'https://prod.hubspot.com/theme-previewer/12345/edit/my-theme'
63+
);
64+
});
65+
66+
it('should return the correct theme preview URL for QA environment', () => {
67+
mockedFindup.mockReturnValue('/src/my-theme/theme.json');
68+
mockedGetEnv.mockReturnValue('qa');
69+
mockedGetHubSpotWebsiteOrigin.mockReturnValue('https://qa.hubspot.com');
70+
71+
const result = getThemePreviewUrl('/path/to/file', 12345);
72+
73+
expect(getEnv).toHaveBeenCalledWith(12345);
74+
expect(getHubSpotWebsiteOrigin).toHaveBeenCalledWith(ENVIRONMENTS.QA);
75+
expect(result).toBe(
76+
'https://qa.hubspot.com/theme-previewer/12345/edit/my-theme'
77+
);
78+
});
79+
80+
it('should return undefined if theme.json is not found', () => {
81+
mockedFindup.mockReturnValue(null);
82+
83+
const result = getThemePreviewUrl('/invalid/path', 12345);
84+
85+
expect(result).toBeUndefined();
86+
});
87+
});
88+
});

lib/__tests__/validate.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import fs, { Stats } from 'fs-extra';
2+
import { validateHubl } from '../../api/validateHubl';
3+
import { walk } from '../fs';
4+
import { lint } from '../cms/validate';
5+
import { LintResult, Validation } from '../../types/HublValidation';
6+
import { AxiosPromise } from 'axios';
7+
8+
jest.mock('fs-extra');
9+
jest.mock('../../api/validateHubl');
10+
jest.mock('../fs');
11+
12+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13+
const mockedFsStat = fs.stat as jest.MockedFunction<any>;
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
const mockedFsReadFile = fs.readFile as jest.MockedFunction<any>;
16+
const mockedWalk = walk as jest.MockedFunction<typeof walk>;
17+
const mockedValidateHubl = validateHubl as jest.MockedFunction<
18+
typeof validateHubl
19+
>;
20+
21+
const mockFsStats = jest.createMockFromModule<Stats>('fs-extra');
22+
23+
mockFsStats.isDirectory = jest.fn(() => true);
24+
25+
const mockValidation: Validation = {
26+
meta: {
27+
all_widgets: [],
28+
widgets_in_rich_text: [],
29+
editable_flex_areas: [],
30+
editable_layout_sections: null,
31+
email_style_settings: null,
32+
sms_flex_area: [],
33+
google_font_variations: null,
34+
custom_font_variations: [],
35+
has_style_tag: false,
36+
has_header_tag: false,
37+
output_html: '',
38+
has_menu_tag: false,
39+
has_theme_setting_function: false,
40+
template_source: '',
41+
attribute_defaults: null,
42+
template_errors: [],
43+
path_dependencies: [],
44+
theme_field_dependencies: [],
45+
template_type_ids: null,
46+
exact_path_references: [],
47+
required_scopes_to_render: [],
48+
},
49+
renderingErrors: [],
50+
html: '',
51+
};
52+
53+
describe('lib/cms/validate', () => {
54+
const accountId = 123;
55+
const filePath = 'test.html';
56+
57+
it('should return an empty array if directory has no files', async () => {
58+
mockedFsStat.mockResolvedValue(mockFsStats);
59+
mockedWalk.mockResolvedValue([]);
60+
61+
const result = await lint(accountId, filePath);
62+
expect(result).toEqual([]);
63+
});
64+
65+
it('should return the correct object if a file has no content', async () => {
66+
mockedFsStat.mockResolvedValue({ isDirectory: () => false });
67+
mockedFsReadFile.mockResolvedValue(' ');
68+
69+
const result = await lint(accountId, filePath);
70+
expect(result).toEqual([{ file: filePath, validation: null }]);
71+
});
72+
73+
it('should call validateHubl with the correct parameters', async () => {
74+
const mockSource = 'valid HUBL content';
75+
mockedFsStat.mockResolvedValue({ isDirectory: () => false });
76+
mockedFsReadFile.mockResolvedValue(mockSource);
77+
mockedValidateHubl.mockResolvedValue({
78+
data: mockValidation,
79+
} as unknown as AxiosPromise<Validation>);
80+
const result = await lint(accountId, filePath);
81+
expect(validateHubl).toHaveBeenCalledWith(accountId, mockSource);
82+
expect(result).toEqual([{ file: filePath, validation: mockValidation }]);
83+
});
84+
85+
it('should filter out files with invalid extensions', async () => {
86+
const invalidFile = 'test.txt';
87+
mockedFsStat.mockResolvedValue({ isDirectory: () => true });
88+
mockedWalk.mockResolvedValue([invalidFile, filePath]);
89+
mockedFsReadFile.mockResolvedValue('valid HUBL content');
90+
mockedValidateHubl.mockResolvedValue({
91+
data: mockValidation,
92+
} as unknown as AxiosPromise<Validation>);
93+
94+
const result = await lint(accountId, filePath);
95+
96+
expect(result).toHaveLength(1);
97+
expect((result as Partial<LintResult>[])[0].file).toBe(filePath);
98+
});
99+
100+
it('should execute callback if provided', async () => {
101+
const mockCallback = jest.fn();
102+
const mockSource = 'valid HUBL content';
103+
mockedFsStat.mockResolvedValue({ isDirectory: () => false });
104+
mockedFsReadFile.mockResolvedValue(mockSource);
105+
mockedValidateHubl.mockResolvedValue({
106+
data: mockValidation,
107+
} as unknown as AxiosPromise<Validation>);
108+
109+
await lint(accountId, filePath, mockCallback);
110+
expect(mockCallback).toHaveBeenCalledWith({
111+
file: filePath,
112+
validation: mockValidation,
113+
});
114+
});
115+
});

0 commit comments

Comments
 (0)