@squiz/dxp-cli-next 5.30.0-develop.2 → 5.30.0-develop.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,9 @@ const createActivateCommand = () => {
37
37
  write(chalk_1.default.red(str));
38
38
  },
39
39
  })
40
+ .addOption(new commander_1.Option('-r, --region <string>', 'Region for your cdp instance to be activated e.g. au')
41
+ .choices(['au', 'us', 'uk'])
42
+ .default(''))
40
43
  .action((options) => __awaiter(void 0, void 0, void 0, function* () {
41
44
  yield (0, utils_1.throwErrorIfNotLoggedIn)(activateCommand);
42
45
  console.log('');
@@ -47,10 +50,22 @@ const createActivateCommand = () => {
47
50
  return status < 400 || status === 404;
48
51
  },
49
52
  });
50
- const scvDeployBaseUrl = yield (0, utils_1.buildDXPUrl)(constants_1.SCV_DEPLOY_SERVICE_NAME);
51
- const apiUrl = `${scvDeployBaseUrl.dxpUrl}/${scvDeployBaseUrl.tenant}`;
53
+ const scvDeployBaseUrl = yield (0, utils_1.buildDXPUrl)(constants_1.SCV_DEPLOY_SERVICE_NAME, '', '', options.region);
54
+ let apiUrl = '';
55
+ const tenant = scvDeployBaseUrl.tenant;
56
+ const hasRegion = options.region !== '';
57
+ // If the region is provided, use the region specific URL
58
+ // Otherwise, we utilize service to automatically retrieve the region for us
59
+ if (hasRegion) {
60
+ apiUrl = `${scvDeployBaseUrl.dxpUrl}/${tenant}`;
61
+ }
62
+ else {
63
+ apiUrl = `${scvDeployBaseUrl.dxpUrl}`;
64
+ }
52
65
  const getDeployResponse = (yield apiService.client
53
- .get(apiUrl)
66
+ .get(apiUrl, {
67
+ headers: Object.assign({ 'Content-Type': 'application/json' }, (!hasRegion && { 'x-dxp-tenant': tenant })),
68
+ })
54
69
  .catch((err) => {
55
70
  (0, utils_1.logDebug)(`RAW ERROR: ${JSON.stringify(err)}`);
56
71
  if (err.response && err.response.status != 404) {
@@ -62,7 +77,9 @@ const createActivateCommand = () => {
62
77
  }
63
78
  (0, utils_1.logDebug)(`PUT ${apiUrl}`);
64
79
  const activateInstanceAndDeploySchemaResponse = (yield apiService.client
65
- .put(apiUrl)
80
+ .put(apiUrl, {
81
+ headers: Object.assign({ 'Content-Type': 'application/json' }, (!hasRegion && { 'x-dxp-tenant': tenant })),
82
+ })
66
83
  .catch((err) => {
67
84
  (0, utils_1.logDebug)(`RAW ERROR: ${JSON.stringify(err)}`);
68
85
  if (err.response) {
@@ -42,7 +42,7 @@ const activate = __importStar(require("./activate"));
42
42
  const utils = __importStar(require("../../utils"));
43
43
  const axios_1 = __importDefault(require("axios"));
44
44
  jest.mock('axios');
45
- const mockDomainWithPath = 'http://localhost:9999/__dxp/us/scv-deploy/myTenant';
45
+ const mockDomainWithPath = 'http://localhost:9999/__dxp/service/scv-deploy/myTenant';
46
46
  function createMockArgs() {
47
47
  return ['node', 'dxp-cli', 'cdp', 'instance', 'activate'];
48
48
  }
@@ -65,103 +65,216 @@ describe('cdpInstanceCommand', () => {
65
65
  afterEach(() => {
66
66
  jest.clearAllMocks();
67
67
  });
68
- it('should throw error when tenant exists', () => __awaiter(void 0, void 0, void 0, function* () {
69
- const mockedAxiosInstance = {
70
- get: jest.fn().mockResolvedValue({ status: 200 }),
71
- interceptors: {
72
- request: { use: jest.fn() },
73
- response: { use: jest.fn() },
74
- },
75
- };
76
- axios_1.default.create.mockReturnValue(mockedAxiosInstance);
77
- jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
78
- dxpUrl: `${mockDomain}/__dxp/us/scv-deploy`,
79
- tenant: mockTenant,
80
- });
81
- const mockPath = (0, utils_1.createMockUrl)(mockRegion, mockTenant);
82
- expect(`${mockDomain}${mockPath}`).toEqual(mockDomainWithPath);
83
- const program = (0, activate_1.default)();
84
- yield program.parseAsync(createMockArgs());
85
- expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining(activate_1.errorMessage));
86
- }));
87
- it('deploys a default schema and activate an instance', () => __awaiter(void 0, void 0, void 0, function* () {
88
- jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
89
- dxpUrl: `${mockDomain}/__dxp/us/scv-deploy`,
90
- tenant: mockTenant,
91
- });
92
- jest.spyOn(utils, 'pollForDeployedSchema').mockResolvedValue({});
93
- const mockedAxiosInstance = {
94
- get: jest.fn().mockResolvedValue({ status: 404 }),
95
- put: jest
96
- .fn()
97
- .mockResolvedValue({ status: 200, data: { message: 'Success' } }),
98
- interceptors: {
99
- request: { use: jest.fn() },
100
- response: { use: jest.fn() },
101
- },
102
- };
103
- axios_1.default.create.mockReturnValue(mockedAxiosInstance);
104
- const mockPath = (0, utils_1.createMockUrl)(mockRegion, mockTenant);
105
- expect(`${mockDomain}${mockPath}`).toEqual(mockDomainWithPath);
106
- const program = (0, activate_1.default)();
107
- yield program.parseAsync(createMockArgs());
108
- expect(mockedAxiosInstance.put).toHaveBeenCalledWith(`${mockDomain}${mockPath}`);
109
- // Note the output from the spinner doesn't seem to appear here but this still tests that it
110
- // ran without displaying an error.
111
- expect(logSpy).toHaveBeenNthCalledWith(1, '');
112
- expect(logSpy).toHaveBeenNthCalledWith(2, '');
113
- expect(logSpy).toHaveBeenNthCalledWith(3, 'Your Schema has been deployed and instance has been activated.');
114
- expect(logSpy).toHaveBeenNthCalledWith(4, '');
115
- expect(errorSpy).not.toHaveBeenCalled();
116
- }));
117
- it('should throw error when trying to activate an instance currently', () => __awaiter(void 0, void 0, void 0, function* () {
118
- jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
119
- dxpUrl: `${mockDomain}/__dxp/us/scv-deploy`,
120
- tenant: mockTenant,
121
- });
122
- jest.spyOn(utils, 'pollForDeployedSchema').mockResolvedValue({});
123
- const mockedAxios = axios_1.default;
124
- //GET request (404)
125
- mockedAxios.get.mockResolvedValueOnce({
126
- status: 404,
127
- data: { status: deploy_const_1.CDP_DEPLOY_STATUS_ERROR },
128
- });
129
- // PUT request (409)
130
- mockedAxios.put.mockResolvedValueOnce({
131
- status: 409,
132
- data: {
133
- tenantid: 'myTenant',
134
- version: 'unknown',
135
- stack: 'stackName',
136
- status: deploy_const_1.CDP_DEPLOY_STATUS_DEPLOYING,
137
- },
138
- });
139
- axios_1.default.create.mockReturnValue(mockedAxios);
140
- const mockPath = (0, utils_1.createMockUrl)(mockRegion, mockTenant);
141
- expect(`${mockDomain}${mockPath}`).toEqual(mockDomainWithPath);
142
- const program = (0, activate_1.default)();
143
- yield program.parseAsync(createMockArgs());
144
- expect(mockedAxios.put).toHaveBeenCalledWith(`${mockDomain}${mockPath}`);
145
- expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Currently activating instance. Please try again later.'));
146
- }));
147
- it('should throw error when get tenant returns 403', () => __awaiter(void 0, void 0, void 0, function* () {
148
- const mockedAxiosInstance = {
149
- get: jest.fn().mockRejectedValue({
150
- response: { status: 403, data: {} },
151
- }),
152
- interceptors: {
153
- request: { use: jest.fn() },
154
- response: { use: jest.fn() },
155
- },
156
- };
157
- axios_1.default.create.mockReturnValue(mockedAxiosInstance);
158
- jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
159
- dxpUrl: `${mockDomain}/__dxp/us/scv-deploy`,
160
- tenant: mockTenant,
161
- });
162
- const handleActivateErrorSpy = jest.spyOn(activate, 'handleActivateError');
163
- const program = (0, activate_1.default)();
164
- yield program.parseAsync(createMockArgs());
165
- expect(handleActivateErrorSpy).toHaveBeenCalledWith(403, {});
166
- }));
68
+ describe('when region is provided', () => {
69
+ it.each(['au', 'us', 'uk'])('should throw error when tenant exists for %s region', (region) => __awaiter(void 0, void 0, void 0, function* () {
70
+ const mockedAxiosInstance = {
71
+ get: jest.fn().mockResolvedValue({ status: 200 }),
72
+ interceptors: {
73
+ request: { use: jest.fn() },
74
+ response: { use: jest.fn() },
75
+ },
76
+ };
77
+ axios_1.default.create.mockReturnValue(mockedAxiosInstance);
78
+ jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
79
+ dxpUrl: `${mockDomain}/__dxp/${region}/scv-deploy`,
80
+ tenant: mockTenant,
81
+ });
82
+ const program = (0, activate_1.default)();
83
+ yield program.parseAsync((0, utils_1.createMockActivateArgs)(region));
84
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining(activate_1.errorMessage));
85
+ }));
86
+ it.each(['au', 'us', 'uk'])('deploys a default schema and activate an instance for %s region', (region) => __awaiter(void 0, void 0, void 0, function* () {
87
+ jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
88
+ dxpUrl: `${mockDomain}/__dxp/${region}/scv-deploy`,
89
+ tenant: mockTenant,
90
+ });
91
+ jest.spyOn(utils, 'pollForDeployedSchema').mockResolvedValue({});
92
+ const mockedAxiosInstance = {
93
+ get: jest.fn().mockResolvedValue({ status: 404 }),
94
+ put: jest
95
+ .fn()
96
+ .mockResolvedValue({ status: 200, data: { message: 'Success' } }),
97
+ interceptors: {
98
+ request: { use: jest.fn() },
99
+ response: { use: jest.fn() },
100
+ },
101
+ };
102
+ axios_1.default.create.mockReturnValue(mockedAxiosInstance);
103
+ const program = (0, activate_1.default)();
104
+ yield program.parseAsync((0, utils_1.createMockActivateArgs)(region));
105
+ expect(mockedAxiosInstance.put).toHaveBeenCalledWith(`http://localhost:9999/__dxp/${region}/scv-deploy/${mockTenant}`, {
106
+ headers: {
107
+ 'Content-Type': 'application/json',
108
+ },
109
+ });
110
+ expect(logSpy).toHaveBeenNthCalledWith(1, '');
111
+ expect(logSpy).toHaveBeenNthCalledWith(2, '');
112
+ expect(logSpy).toHaveBeenNthCalledWith(3, 'Your Schema has been deployed and instance has been activated.');
113
+ expect(logSpy).toHaveBeenNthCalledWith(4, '');
114
+ expect(errorSpy).not.toHaveBeenCalled();
115
+ }));
116
+ it.each(['au', 'us', 'uk'])('should throw error when trying to activate an instance currently for %s region', (region) => __awaiter(void 0, void 0, void 0, function* () {
117
+ jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
118
+ dxpUrl: `${mockDomain}/__dxp/${region}/scv-deploy`,
119
+ tenant: mockTenant,
120
+ });
121
+ jest.spyOn(utils, 'pollForDeployedSchema').mockResolvedValue({});
122
+ const mockedAxios = axios_1.default;
123
+ //GET request (404)
124
+ mockedAxios.get.mockResolvedValueOnce({
125
+ status: 404,
126
+ data: { status: deploy_const_1.CDP_DEPLOY_STATUS_ERROR },
127
+ });
128
+ // PUT request (409)
129
+ mockedAxios.put.mockResolvedValueOnce({
130
+ status: 409,
131
+ data: {
132
+ tenantid: 'myTenant',
133
+ version: 'unknown',
134
+ stack: 'stackName',
135
+ status: deploy_const_1.CDP_DEPLOY_STATUS_DEPLOYING,
136
+ },
137
+ });
138
+ axios_1.default.create.mockReturnValue(mockedAxios);
139
+ const program = (0, activate_1.default)();
140
+ yield program.parseAsync((0, utils_1.createMockActivateArgs)(region));
141
+ expect(mockedAxios.put).toHaveBeenCalledWith(`http://localhost:9999/__dxp/${region}/scv-deploy/${mockTenant}`, {
142
+ headers: {
143
+ 'Content-Type': 'application/json',
144
+ },
145
+ });
146
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Currently activating instance. Please try again later.'));
147
+ }));
148
+ it.each(['au', 'us', 'uk'])('should throw error when get tenant returns 403 for %s region', (region) => __awaiter(void 0, void 0, void 0, function* () {
149
+ const mockedAxiosInstance = {
150
+ get: jest.fn().mockRejectedValue({
151
+ response: { status: 403, data: {} },
152
+ }),
153
+ interceptors: {
154
+ request: { use: jest.fn() },
155
+ response: { use: jest.fn() },
156
+ },
157
+ };
158
+ axios_1.default.create.mockReturnValue(mockedAxiosInstance);
159
+ jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
160
+ dxpUrl: `${mockDomain}/__dxp/${region}/scv-deploy`,
161
+ tenant: mockTenant,
162
+ });
163
+ const handleActivateErrorSpy = jest.spyOn(activate, 'handleActivateError');
164
+ const program = (0, activate_1.default)();
165
+ yield program.parseAsync((0, utils_1.createMockActivateArgs)(region));
166
+ expect(handleActivateErrorSpy).toHaveBeenCalledWith(403, {});
167
+ }));
168
+ });
169
+ describe('when region is not provided', () => {
170
+ it('should throw error when tenant exists', () => __awaiter(void 0, void 0, void 0, function* () {
171
+ const mockedAxiosInstance = {
172
+ get: jest.fn().mockResolvedValue({ status: 200 }),
173
+ interceptors: {
174
+ request: { use: jest.fn() },
175
+ response: { use: jest.fn() },
176
+ },
177
+ };
178
+ axios_1.default.create.mockReturnValue(mockedAxiosInstance);
179
+ jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
180
+ dxpUrl: `${mockDomain}/__dxp/service/scv-deploy`,
181
+ tenant: mockTenant,
182
+ });
183
+ const mockPath = (0, utils_1.createMockUrl)(mockTenant);
184
+ expect(`${mockDomain}${mockPath}`).toEqual(mockDomainWithPath);
185
+ const program = (0, activate_1.default)();
186
+ yield program.parseAsync(createMockArgs());
187
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining(activate_1.errorMessage));
188
+ }));
189
+ it('deploys a default schema and activate an instance', () => __awaiter(void 0, void 0, void 0, function* () {
190
+ jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
191
+ dxpUrl: `${mockDomain}/__dxp/service/scv-deploy`,
192
+ tenant: mockTenant,
193
+ });
194
+ jest.spyOn(utils, 'pollForDeployedSchema').mockResolvedValue({});
195
+ const mockedAxiosInstance = {
196
+ get: jest.fn().mockResolvedValue({ status: 404 }),
197
+ put: jest
198
+ .fn()
199
+ .mockResolvedValue({ status: 200, data: { message: 'Success' } }),
200
+ interceptors: {
201
+ request: { use: jest.fn() },
202
+ response: { use: jest.fn() },
203
+ },
204
+ };
205
+ axios_1.default.create.mockReturnValue(mockedAxiosInstance);
206
+ const mockPath = (0, utils_1.createMockUrl)(mockTenant);
207
+ expect(`${mockDomain}${mockPath}`).toEqual(mockDomainWithPath);
208
+ const program = (0, activate_1.default)();
209
+ yield program.parseAsync(createMockArgs());
210
+ expect(mockedAxiosInstance.put).toHaveBeenCalledWith('http://localhost:9999/__dxp/service/scv-deploy', {
211
+ headers: {
212
+ 'Content-Type': 'application/json',
213
+ 'x-dxp-tenant': 'myTenant',
214
+ },
215
+ });
216
+ // Note the output from the spinner doesn't seem to appear here but this still tests that it
217
+ // ran without displaying an error.
218
+ expect(logSpy).toHaveBeenNthCalledWith(1, '');
219
+ expect(logSpy).toHaveBeenNthCalledWith(2, '');
220
+ expect(logSpy).toHaveBeenNthCalledWith(3, 'Your Schema has been deployed and instance has been activated.');
221
+ expect(logSpy).toHaveBeenNthCalledWith(4, '');
222
+ expect(errorSpy).not.toHaveBeenCalled();
223
+ }));
224
+ it('should throw error when trying to activate an instance currently', () => __awaiter(void 0, void 0, void 0, function* () {
225
+ jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
226
+ dxpUrl: `${mockDomain}/__dxp/service/scv-deploy`,
227
+ tenant: mockTenant,
228
+ });
229
+ jest.spyOn(utils, 'pollForDeployedSchema').mockResolvedValue({});
230
+ const mockedAxios = axios_1.default;
231
+ //GET request (404)
232
+ mockedAxios.get.mockResolvedValueOnce({
233
+ status: 404,
234
+ data: { status: deploy_const_1.CDP_DEPLOY_STATUS_ERROR },
235
+ });
236
+ // PUT request (409)
237
+ mockedAxios.put.mockResolvedValueOnce({
238
+ status: 409,
239
+ data: {
240
+ tenantid: 'myTenant',
241
+ version: 'unknown',
242
+ stack: 'stackName',
243
+ status: deploy_const_1.CDP_DEPLOY_STATUS_DEPLOYING,
244
+ },
245
+ });
246
+ axios_1.default.create.mockReturnValue(mockedAxios);
247
+ const mockPath = (0, utils_1.createMockUrl)(mockTenant);
248
+ expect(`${mockDomain}${mockPath}`).toEqual(mockDomainWithPath);
249
+ const program = (0, activate_1.default)();
250
+ yield program.parseAsync(createMockArgs());
251
+ expect(mockedAxios.put).toHaveBeenCalledWith('http://localhost:9999/__dxp/service/scv-deploy', {
252
+ headers: {
253
+ 'Content-Type': 'application/json',
254
+ 'x-dxp-tenant': 'myTenant',
255
+ },
256
+ });
257
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Currently activating instance. Please try again later.'));
258
+ }));
259
+ it('should throw error when get tenant returns 403', () => __awaiter(void 0, void 0, void 0, function* () {
260
+ const mockedAxiosInstance = {
261
+ get: jest.fn().mockRejectedValue({
262
+ response: { status: 403, data: {} },
263
+ }),
264
+ interceptors: {
265
+ request: { use: jest.fn() },
266
+ response: { use: jest.fn() },
267
+ },
268
+ };
269
+ axios_1.default.create.mockReturnValue(mockedAxiosInstance);
270
+ jest.spyOn(utils, 'buildDXPUrl').mockResolvedValue({
271
+ dxpUrl: `${mockDomain}/__dxp/service/scv-deploy`,
272
+ tenant: mockTenant,
273
+ });
274
+ const handleActivateErrorSpy = jest.spyOn(activate, 'handleActivateError');
275
+ const program = (0, activate_1.default)();
276
+ yield program.parseAsync(createMockArgs());
277
+ expect(handleActivateErrorSpy).toHaveBeenCalledWith(403, {});
278
+ }));
279
+ });
167
280
  });
@@ -60,7 +60,7 @@ describe('cdpSchemaCommand', () => {
60
60
  jest.clearAllMocks(); // Clear all spies
61
61
  });
62
62
  it('correctly handles command arguments', () => __awaiter(void 0, void 0, void 0, function* () {
63
- const mockPath = (0, utils_1.createMockUrl)(mockRegion, mockTenant);
63
+ const mockPath = (0, utils_1.createMockUrl)(mockTenant, mockRegion);
64
64
  (0, nock_1.default)(mockDomain)
65
65
  .put(mockPath, require(path_1.default.resolve(process.cwd(), './src/__tests__/cdp/scv/schema.json')))
66
66
  .reply(200, {
@@ -81,7 +81,7 @@ describe('cdpSchemaCommand', () => {
81
81
  }));
82
82
  it('presents a formatted invalid schema message', () => __awaiter(void 0, void 0, void 0, function* () {
83
83
  const schemaFileContent = require(path_1.default.resolve(process.cwd(), mockFilePath));
84
- const mockPath = (0, utils_1.createMockUrl)(mockRegion, mockTenant);
84
+ const mockPath = (0, utils_1.createMockUrl)(mockTenant, mockRegion);
85
85
  expect(`${mockDomain}${mockPath}`).toEqual('http://localhost:9999/__dxp/us/scv-deploy/myTenant');
86
86
  (0, nock_1.default)(mockDomain)
87
87
  .get(mockPath)
@@ -110,7 +110,7 @@ describe('cdpSchemaCommand', () => {
110
110
  }));
111
111
  it('deploys a schema', () => __awaiter(void 0, void 0, void 0, function* () {
112
112
  const schemaFileContent = require(path_1.default.resolve(process.cwd(), mockFilePath));
113
- const mockPath = (0, utils_1.createMockUrl)(mockRegion, mockTenant);
113
+ const mockPath = (0, utils_1.createMockUrl)(mockTenant, mockRegion);
114
114
  expect(`${mockDomain}${mockPath}`).toEqual('http://localhost:9999/__dxp/us/scv-deploy/myTenant');
115
115
  (0, nock_1.default)(mockDomain)
116
116
  .get(mockPath)
@@ -14,7 +14,8 @@ export declare function buildDXPUrl(serviceName: string, tenantID?: string, over
14
14
  tenant: string | undefined;
15
15
  }>;
16
16
  export declare function handleCommandError(error: Error): void;
17
- export declare function createMockUrl(region: string, path: string): string;
17
+ export declare function createMockUrl(path: string, region?: string): string;
18
+ export declare function createMockActivateArgs(region: string): Array<string>;
18
19
  /**
19
20
  * Poll the schema to be deployed.
20
21
  */
package/lib/cdp/utils.js CHANGED
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.pollForDeployedSchema = exports.createMockUrl = exports.handleCommandError = exports.buildDXPUrl = exports.throwErrorIfNotLoggedIn = exports.logDebug = void 0;
15
+ exports.pollForDeployedSchema = exports.createMockActivateArgs = exports.createMockUrl = exports.handleCommandError = exports.buildDXPUrl = exports.throwErrorIfNotLoggedIn = exports.logDebug = void 0;
16
16
  const ApplicationStore_1 = require("../ApplicationStore");
17
17
  const chalk_1 = __importDefault(require("chalk"));
18
18
  const ApplicationConfig_1 = require("../ApplicationConfig");
@@ -43,14 +43,14 @@ function buildDXPUrl(serviceName, tenantID, override, region) {
43
43
  const existingConfig = yield (0, ApplicationConfig_1.fetchApplicationConfig)(tenantID);
44
44
  logDebug(`existingConfig: ${JSON.stringify(existingConfig)}`);
45
45
  return {
46
- dxpUrl: `${existingConfig.baseUrl}/__dxp/${region || existingConfig.region}/${serviceName}`,
46
+ dxpUrl: `${existingConfig.baseUrl}/__dxp/${region || 'service'}/${serviceName}`,
47
47
  tenant: existingConfig.tenant,
48
48
  };
49
49
  }
50
50
  else {
51
51
  logDebug(`Using override URL: ${override}`);
52
52
  return {
53
- dxpUrl: `${override}/__dxp/${region || 'au'}/${serviceName}`,
53
+ dxpUrl: `${override}/__dxp/${region || 'service'}/${serviceName}`,
54
54
  tenant: tenantID,
55
55
  };
56
56
  }
@@ -82,10 +82,14 @@ function handleCommandError(error) {
82
82
  }
83
83
  }
84
84
  exports.handleCommandError = handleCommandError;
85
- function createMockUrl(region, path) {
86
- return `/__dxp/${region}/scv-deploy/${path}`;
85
+ function createMockUrl(path, region) {
86
+ return `/__dxp/${region || 'service'}/scv-deploy/${path}`;
87
87
  }
88
88
  exports.createMockUrl = createMockUrl;
89
+ function createMockActivateArgs(region) {
90
+ return ['node', 'dxp-cli', 'cdp', 'instance', 'activate', '--region', region];
91
+ }
92
+ exports.createMockActivateArgs = createMockActivateArgs;
89
93
  /**
90
94
  * Poll the schema to be deployed.
91
95
  */
@@ -19,6 +19,7 @@ const ApiService_1 = require("../../../ApiService");
19
19
  const ApplicationConfig_1 = require("../../../ApplicationConfig");
20
20
  const constants_1 = require("../../../constants");
21
21
  const definitions_1 = require("../../utils/definitions");
22
+ const validation_1 = require("../validation");
22
23
  const dx_logger_lib_1 = require("@squiz/dx-logger-lib");
23
24
  exports.logger = (0, dx_logger_lib_1.getLogger)({
24
25
  name: 'upload-layout',
@@ -52,7 +53,7 @@ const createDeployCommand = () => {
52
53
  const layout = yield (0, definitions_1.loadLayoutDefinition)(layoutFile);
53
54
  if (layout !== undefined) {
54
55
  // Pre-deployment validation: check zones match between YAML and template
55
- const zoneValidationError = validateZoneConsistency(layout);
56
+ const zoneValidationError = (0, validation_1.validateZoneConsistency)(layout);
56
57
  if (zoneValidationError) {
57
58
  throw new Error(zoneValidationError);
58
59
  }
@@ -119,48 +120,3 @@ function maybeGetApplicationConfig() {
119
120
  catch (_a) { }
120
121
  });
121
122
  }
122
- /**
123
- * Validates that zones defined in YAML match zones used in the Handlebars template
124
- * @param layout The layout definition containing zones and template
125
- * @returns Error message if validation fails, null if validation passes
126
- */
127
- function validateZoneConsistency(layout) {
128
- const yamlZones = Object.keys(layout.zones || {});
129
- const template = layout.template;
130
- // If no template is provided, skip validation
131
- if (!template) {
132
- return null;
133
- }
134
- // Extract zone references from Handlebars template
135
- // Look for patterns like {{#each zones.zoneName}}, {{zones.zoneName}}, {{#if zones.zoneName}}
136
- const zonePattern = /\{\{(?:#(?:each|if)\s+)?zones\.(\w+)/g;
137
- const templateZones = new Set();
138
- let match;
139
- // Loop through the template and extract the zone names
140
- while ((match = zonePattern.exec(template)) !== null) {
141
- templateZones.add(match[1]);
142
- }
143
- // Convert the set to an array
144
- const templateZoneArray = Array.from(templateZones);
145
- // Find zones defined in YAML but not used in template
146
- const unusedYamlZones = yamlZones.filter(zone => !templateZones.has(zone));
147
- // Find zones used in template but not defined in YAML
148
- const undefinedTemplateZones = templateZoneArray.filter(zone => !yamlZones.includes(zone));
149
- // Create an array of errors
150
- const errors = [];
151
- // Add the unused zones to the errors
152
- if (unusedYamlZones.length > 0) {
153
- errors.push(`Zones defined in YAML but not used in template: ${unusedYamlZones.join(', ')}`);
154
- }
155
- // Add the undefined zones to the errors
156
- if (undefinedTemplateZones.length > 0) {
157
- errors.push(`Zones used in template but not defined in YAML: ${undefinedTemplateZones.join(', ')}`);
158
- }
159
- // If there are errors, return the errors
160
- if (errors.length > 0) {
161
- return `Zone consistency validation failed:\n${errors
162
- .map(err => ` - ${err}`)
163
- .join('\n')}`;
164
- }
165
- return null;
166
- }
@@ -0,0 +1 @@
1
+ export { validateZoneConsistency } from './zone-consistency';
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateZoneConsistency = void 0;
4
+ var zone_consistency_1 = require("./zone-consistency");
5
+ Object.defineProperty(exports, "validateZoneConsistency", { enumerable: true, get: function () { return zone_consistency_1.validateZoneConsistency; } });
@@ -0,0 +1,7 @@
1
+ import { LayoutDefinition } from '../../utils/definitions';
2
+ /**
3
+ * Validates that zones defined in YAML match zones used in the Handlebars template
4
+ * @param layout The layout definition containing zones and template
5
+ * @returns Error message if validation fails, null if validation passes
6
+ */
7
+ export declare function validateZoneConsistency(layout: LayoutDefinition): string | null;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateZoneConsistency = void 0;
4
+ /**
5
+ * Validates that zones defined in YAML match zones used in the Handlebars template
6
+ * @param layout The layout definition containing zones and template
7
+ * @returns Error message if validation fails, null if validation passes
8
+ */
9
+ function validateZoneConsistency(layout) {
10
+ const yamlZones = Object.keys(layout.zones || {});
11
+ const template = layout.template;
12
+ // If no template is provided, skip validation
13
+ if (!template) {
14
+ return null;
15
+ }
16
+ // Extract zone references from Handlebars template
17
+ // Look for patterns like {{#each zones.zoneName}}, {{zones.zoneName}}, {{#if zones.zoneName}}
18
+ const zonePattern = /\{\{(?:#(?:each|if)\s+)?zones\.(\w+)/g;
19
+ const templateZones = new Set();
20
+ let match;
21
+ // Loop through the template and extract the zone names
22
+ while ((match = zonePattern.exec(template)) !== null) {
23
+ templateZones.add(match[1]);
24
+ }
25
+ // Convert the set to an array
26
+ const templateZoneArray = Array.from(templateZones);
27
+ // Find zones defined in YAML but not used in template
28
+ const unusedYamlZones = yamlZones.filter(zone => !templateZones.has(zone));
29
+ // Find zones used in template but not defined in YAML
30
+ const undefinedTemplateZones = templateZoneArray.filter(zone => !yamlZones.includes(zone));
31
+ // Create an array of errors
32
+ const errors = [];
33
+ // Add the unused zones to the errors
34
+ if (unusedYamlZones.length > 0) {
35
+ errors.push(`Zones defined in YAML but not used in template: ${unusedYamlZones.join(', ')}`);
36
+ }
37
+ // Add the undefined zones to the errors
38
+ if (undefinedTemplateZones.length > 0) {
39
+ errors.push(`Zones used in template but not defined in YAML: ${undefinedTemplateZones.join(', ')}`);
40
+ }
41
+ // If there are errors, return the errors
42
+ if (errors.length > 0) {
43
+ return `Zone consistency validation failed:\n${errors
44
+ .map(err => ` - ${err}`)
45
+ .join('\n')}`;
46
+ }
47
+ return null;
48
+ }
49
+ exports.validateZoneConsistency = validateZoneConsistency;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const zone_consistency_1 = require("./zone-consistency");
4
+ describe('validateZoneConsistency', () => {
5
+ const createMockLayout = (zones, template) => ({
6
+ name: 'test-layout',
7
+ displayName: 'Test Layout',
8
+ description: 'A test layout',
9
+ zones,
10
+ template,
11
+ });
12
+ it('should return null for valid zone consistency', () => {
13
+ const layout = createMockLayout({
14
+ header: { displayName: 'Header', description: 'Header zone' },
15
+ content: { displayName: 'Content', description: 'Content zone' },
16
+ }, '<div>{{zones.header}}</div><div>{{zones.content}}</div>');
17
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
18
+ expect(result).toBeNull();
19
+ });
20
+ it('should return null when no template is provided', () => {
21
+ const layout = createMockLayout({
22
+ header: { displayName: 'Header', description: 'Header zone' },
23
+ }, '');
24
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
25
+ expect(result).toBeNull();
26
+ });
27
+ it('should detect zones defined in YAML but not used in template', () => {
28
+ const layout = createMockLayout({
29
+ header: { displayName: 'Header', description: 'Header zone' },
30
+ sidebar: { displayName: 'Sidebar', description: 'Sidebar zone' },
31
+ }, '<div>{{zones.header}}</div>');
32
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
33
+ expect(result).toContain('Zone consistency validation failed');
34
+ expect(result).toContain('Zones defined in YAML but not used in template: sidebar');
35
+ });
36
+ it('should detect zones used in template but not defined in YAML', () => {
37
+ const layout = createMockLayout({
38
+ header: { displayName: 'Header', description: 'Header zone' },
39
+ }, '<div>{{zones.header}}</div><div>{{zones.footer}}</div>');
40
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
41
+ expect(result).toContain('Zone consistency validation failed');
42
+ expect(result).toContain('Zones used in template but not defined in YAML: footer');
43
+ });
44
+ it('should detect both unused and undefined zones', () => {
45
+ const layout = createMockLayout({
46
+ header: { displayName: 'Header', description: 'Header zone' },
47
+ sidebar: { displayName: 'Sidebar', description: 'Sidebar zone' },
48
+ }, '<div>{{zones.header}}</div><div>{{zones.footer}}</div>');
49
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
50
+ expect(result).toContain('Zone consistency validation failed');
51
+ expect(result).toContain('Zones defined in YAML but not used in template: sidebar');
52
+ expect(result).toContain('Zones used in template but not defined in YAML: footer');
53
+ });
54
+ it('should handle Handlebars each loops', () => {
55
+ const layout = createMockLayout({
56
+ items: { displayName: 'Items', description: 'Items zone' },
57
+ }, '<div>{{#each zones.items}}<p>{{this}}</p>{{/each}}</div>');
58
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
59
+ expect(result).toBeNull();
60
+ });
61
+ it('should handle Handlebars if conditions', () => {
62
+ const layout = createMockLayout({
63
+ optional: { displayName: 'Optional', description: 'Optional zone' },
64
+ }, '<div>{{#if zones.optional}}{{zones.optional}}{{/if}}</div>');
65
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
66
+ expect(result).toBeNull();
67
+ });
68
+ it('should handle complex Handlebars patterns', () => {
69
+ const layout = createMockLayout({
70
+ header: { displayName: 'Header', description: 'Header zone' },
71
+ content: { displayName: 'Content', description: 'Content zone' },
72
+ sidebar: { displayName: 'Sidebar', description: 'Sidebar zone' },
73
+ }, `
74
+ <div>{{zones.header}}</div>
75
+ <div>
76
+ {{#if zones.sidebar}}
77
+ <aside>{{zones.sidebar}}</aside>
78
+ {{/if}}
79
+ <main>{{zones.content}}</main>
80
+ </div>
81
+ `);
82
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
83
+ expect(result).toBeNull();
84
+ });
85
+ it('should handle empty zones object', () => {
86
+ const layout = createMockLayout({}, '<div>Static content only</div>');
87
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
88
+ expect(result).toBeNull();
89
+ });
90
+ it('should handle undefined zones property', () => {
91
+ const layout = {
92
+ name: 'test-layout',
93
+ displayName: 'Test Layout',
94
+ description: 'A test layout',
95
+ zones: undefined,
96
+ template: '<div>Static content only</div>',
97
+ };
98
+ const result = (0, zone_consistency_1.validateZoneConsistency)(layout);
99
+ expect(result).toBeNull();
100
+ });
101
+ });
@@ -0,0 +1,11 @@
1
+ /*!
2
+ * @license
3
+ * Copyright (c) 2025, Squiz Australia Pty Ltd.
4
+ * All rights reserved.
5
+ */
6
+ /**
7
+ * Validates a template file and returns detailed results
8
+ * @param content - The template to validate
9
+ * @returns An array of errors
10
+ */
11
+ export declare function validateTemplateFile(content: string): string[];
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /*!
3
+ * @license
4
+ * Copyright (c) 2025, Squiz Australia Pty Ltd.
5
+ * All rights reserved.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.validateTemplateFile = void 0;
9
+ /**
10
+ * Validates a template file and returns detailed results
11
+ * @param content - The template to validate
12
+ * @returns An array of errors
13
+ */
14
+ function validateTemplateFile(content) {
15
+ const errors = [];
16
+ // Check for top-level HTML structure tags that should not be in body content
17
+ const topLevelTags = [
18
+ { name: 'html', regex: /<html(\s|>)/gi },
19
+ { name: 'head', regex: /<head(\s|>)/gi },
20
+ { name: 'body', regex: /<body(\s|>)/gi },
21
+ { name: 'doctype', regex: /<!DOCTYPE[^>]*>/gi },
22
+ ];
23
+ for (const tag of topLevelTags) {
24
+ if (tag.regex.test(content)) {
25
+ errors.push(`Template should not contain top-level HTML structure tag: <${tag.name}>. Templates should only contain body content.`);
26
+ }
27
+ }
28
+ return errors;
29
+ }
30
+ exports.validateTemplateFile = validateTemplateFile;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const validation_1 = require("./validation");
4
+ describe('Handlebars Template Validation', () => {
5
+ describe('validateTemplateFile', () => {
6
+ it('should validate an empty template', () => {
7
+ const template = '';
8
+ const errors = (0, validation_1.validateTemplateFile)(template);
9
+ expect(errors).toHaveLength(0);
10
+ });
11
+ it('should validate a simple valid template', () => {
12
+ const template = '<div>{{zones.content}}</div>';
13
+ const errors = (0, validation_1.validateTemplateFile)(template);
14
+ expect(errors).toHaveLength(0);
15
+ });
16
+ it('should detect top-level HTML tags', () => {
17
+ const template = '<html><body><div>{{zones.content}}</div></body></html>';
18
+ const errors = (0, validation_1.validateTemplateFile)(template);
19
+ expect(errors.length).toBeGreaterThan(0);
20
+ expect(errors).toEqual(expect.arrayContaining([
21
+ expect.stringContaining('Template should not contain top-level HTML structure tag: <html>'),
22
+ expect.stringContaining('Template should not contain top-level HTML structure tag: <body>'),
23
+ ]));
24
+ });
25
+ it('should detect DOCTYPE declarations', () => {
26
+ const template = '<!DOCTYPE html><div>{{zones.content}}</div>';
27
+ const errors = (0, validation_1.validateTemplateFile)(template);
28
+ expect(errors.length).toBeGreaterThan(0);
29
+ expect(errors).toEqual(expect.arrayContaining([
30
+ expect.stringContaining('Template should not contain top-level HTML structure tag: <doctype>'),
31
+ ]));
32
+ });
33
+ it('should detect head tags', () => {
34
+ const template = '<head><title>Test</title></head><div>{{zones.content}}</div>';
35
+ const errors = (0, validation_1.validateTemplateFile)(template);
36
+ expect(errors.length).toBeGreaterThan(0);
37
+ expect(errors).toEqual(expect.arrayContaining([
38
+ expect.stringContaining('Template should not contain top-level HTML structure tag: <head>'),
39
+ ]));
40
+ });
41
+ it('should allow body content elements', () => {
42
+ const template = `
43
+ <div class="layout">
44
+ <header>{{zones.header}}</header>
45
+ <main>{{zones.content}}</main>
46
+ <aside>{{zones.sidebar}}</aside>
47
+ <footer>{{zones.footer}}</footer>
48
+ </div>
49
+ `;
50
+ const errors = (0, validation_1.validateTemplateFile)(template);
51
+ expect(errors).toHaveLength(0);
52
+ });
53
+ it('should allow complex Handlebars expressions', () => {
54
+ const template = `
55
+ <div class="layout">
56
+ {{#if zones.header}}
57
+ <header>{{zones.header}}</header>
58
+ {{/if}}
59
+ <main>{{zones.content}}</main>
60
+ {{#each zones.sidebar}}
61
+ <aside>{{this}}</aside>
62
+ {{/each}}
63
+ </div>
64
+ `;
65
+ const errors = (0, validation_1.validateTemplateFile)(template);
66
+ expect(errors).toHaveLength(0);
67
+ });
68
+ it('should allow all valid HTML body elements', () => {
69
+ const template = `
70
+ <div>{{zones.content}}</div>
71
+ <p>{{zones.text}}</p>
72
+ <span>{{zones.inline}}</span>
73
+ <section>{{zones.section}}</section>
74
+ <article>{{zones.article}}</article>
75
+ <nav>{{zones.navigation}}</nav>
76
+ <header>{{zones.header}}</header>
77
+ <footer>{{zones.footer}}</footer>
78
+ <aside>{{zones.sidebar}}</aside>
79
+ <main>{{zones.main}}</main>
80
+ <h1>{{zones.title}}</h1>
81
+ <img src="{{zones.image}}" alt="{{zones.alt}}">
82
+ <a href="{{zones.link}}">{{zones.linkText}}</a>
83
+ <ul>{{#each zones.list}}<li>{{this}}</li>{{/each}}</ul>
84
+ `;
85
+ const errors = (0, validation_1.validateTemplateFile)(template);
86
+ expect(errors).toHaveLength(0);
87
+ });
88
+ });
89
+ });
@@ -1,3 +1,8 @@
1
+ /*!
2
+ * @license
3
+ * Copyright (c) 2025, Squiz Australia Pty Ltd.
4
+ * All rights reserved.
5
+ */
1
6
  import { z } from 'zod';
2
7
  export declare function loadLayoutDefinition(layoutFile: string): Promise<LayoutDefinition>;
3
8
  export declare function loadLayoutFromFile(layoutFile: string): Promise<InputLayoutDefinition>;
@@ -1,4 +1,9 @@
1
1
  "use strict";
2
+ /*!
3
+ * @license
4
+ * Copyright (c) 2025, Squiz Australia Pty Ltd.
5
+ * All rights reserved.
6
+ */
2
7
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
8
  if (k2 === undefined) k2 = k;
4
9
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -44,10 +49,13 @@ var __rest = (this && this.__rest) || function (s, e) {
44
49
  };
45
50
  Object.defineProperty(exports, "__esModule", { value: true });
46
51
  exports.LayoutDefinition = exports.InputLayoutDefinition = exports.BaseLayoutDefinition = exports.loadLayoutFromFile = exports.loadLayoutDefinition = void 0;
52
+ // External
47
53
  const fs = __importStar(require("node:fs/promises"));
48
54
  const path = __importStar(require("node:path"));
49
55
  const zod_1 = require("zod");
50
56
  const yaml_1 = require("yaml");
57
+ // Local
58
+ const validation_1 = require("../templates/validation");
51
59
  function loadLayoutDefinition(layoutFile) {
52
60
  return __awaiter(this, void 0, void 0, function* () {
53
61
  try {
@@ -83,12 +91,24 @@ function loadLayoutFromFile(layoutFile) {
83
91
  });
84
92
  }
85
93
  exports.loadLayoutFromFile = loadLayoutFromFile;
94
+ /**
95
+ * Loads a template file from a file system
96
+ * @param layoutDirectory - The path to the layout directory
97
+ * @param templateFile - The path to the template file
98
+ * @returns The template
99
+ */
86
100
  function loadTemplate(layoutDirectory, templateFile) {
87
101
  return __awaiter(this, void 0, void 0, function* () {
88
102
  try {
89
- return yield fs.readFile(path.resolve(layoutDirectory, templateFile), {
103
+ const template = yield fs.readFile(path.resolve(layoutDirectory, templateFile), {
90
104
  encoding: 'utf-8',
91
105
  });
106
+ // Validate the template
107
+ const validationErrors = yield (0, validation_1.validateTemplateFile)(template);
108
+ if (validationErrors.length > 0) {
109
+ throw new Error(validationErrors.join('\n'));
110
+ }
111
+ return template;
92
112
  }
93
113
  catch (e) {
94
114
  throw Error(`Failed loading template file "${templateFile}": ${e.message}`);
@@ -34,6 +34,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
35
  const fs = __importStar(require("node:fs/promises"));
36
36
  const definitions_1 = require("./definitions");
37
+ const validation_1 = require("../templates/validation");
37
38
  jest.mock('node:fs/promises');
38
39
  describe('loadLayoutDefinition', () => {
39
40
  const paintLayoutFileYaml = './some-dir/page-layout.yaml';
@@ -436,3 +437,64 @@ describe('LayoutDefinitionParse', () => {
436
437
  expect(layoutDefinition.zones.main.minNodes).toBe(0);
437
438
  });
438
439
  });
440
+ describe('validateTemplateFile', () => {
441
+ describe('valid templates', () => {
442
+ it('should validate a simple valid template', () => __awaiter(void 0, void 0, void 0, function* () {
443
+ const template = '<div>{{zones.content}}</div>';
444
+ const errors = yield (0, validation_1.validateTemplateFile)(template);
445
+ expect(errors).toHaveLength(0);
446
+ }));
447
+ it('should validate a complex valid template', () => __awaiter(void 0, void 0, void 0, function* () {
448
+ const template = `
449
+ <div class="layout">
450
+ {{#if zones.header}}
451
+ <header>{{zones.header}}</header>
452
+ {{/if}}
453
+ <main>{{zones.content}}</main>
454
+ {{#each zones.sidebar}}
455
+ <aside>{{this}}</aside>
456
+ {{/each}}
457
+ </div>
458
+ `;
459
+ const errors = yield (0, validation_1.validateTemplateFile)(template);
460
+ expect(errors).toHaveLength(0);
461
+ }));
462
+ it('should validate template with options', () => __awaiter(void 0, void 0, void 0, function* () {
463
+ const template = '<div class="{{options.theme}}">{{zones.content}}</div>';
464
+ const errors = yield (0, validation_1.validateTemplateFile)(template);
465
+ expect(errors).toHaveLength(0);
466
+ }));
467
+ it('should allow empty template', () => __awaiter(void 0, void 0, void 0, function* () {
468
+ const template = '';
469
+ const errors = yield (0, validation_1.validateTemplateFile)(template);
470
+ expect(errors).toHaveLength(0);
471
+ }));
472
+ });
473
+ describe('invalid templates', () => {
474
+ it('should detect top-level HTML tags', () => __awaiter(void 0, void 0, void 0, function* () {
475
+ const template = '<html><body>{{zones.content}}</body></html>';
476
+ const errors = yield (0, validation_1.validateTemplateFile)(template);
477
+ expect(errors.length).toBeGreaterThan(0);
478
+ expect(errors).toEqual(expect.arrayContaining([
479
+ expect.stringContaining('Template should not contain top-level HTML structure tag: <html>'),
480
+ expect.stringContaining('Template should not contain top-level HTML structure tag: <body>'),
481
+ ]));
482
+ }));
483
+ it('should detect DOCTYPE declarations', () => __awaiter(void 0, void 0, void 0, function* () {
484
+ const template = '<!DOCTYPE html><div>{{zones.content}}</div>';
485
+ const errors = yield (0, validation_1.validateTemplateFile)(template);
486
+ expect(errors.length).toBeGreaterThan(0);
487
+ expect(errors).toEqual(expect.arrayContaining([
488
+ expect.stringContaining('Template should not contain top-level HTML structure tag: <doctype>'),
489
+ ]));
490
+ }));
491
+ });
492
+ describe('error handling', () => {
493
+ it('should handle null template gracefully', () => __awaiter(void 0, void 0, void 0, function* () {
494
+ // Test with a null template
495
+ const template = null;
496
+ const errors = yield (0, validation_1.validateTemplateFile)(template);
497
+ expect(errors).toHaveLength(0);
498
+ }));
499
+ });
500
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/dxp-cli-next",
3
- "version": "5.30.0-develop.2",
3
+ "version": "5.30.0-develop.4",
4
4
  "repository": {
5
5
  "url": "https://gitlab.squiz.net/dxp/dxp-cli-next"
6
6
  },