@izara_project/izara-core-generate-service-code 1.0.58 → 1.0.59

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izara_project/izara-core-generate-service-code",
3
- "version": "1.0.58",
3
+ "version": "1.0.59",
4
4
  "description": "Code for locally generating per service files",
5
5
  "homepage": "https://bitbucket.org/izara-core-support-services/izara-core-support-services-generate-service-code#readme",
6
6
  "repository": {
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
+ import os from 'os';
4
5
  import libs from '../generateResources/IntTest/libs/libs.js';
5
6
  import { getObjectSchema } from '@izara_project/izara-core-library-service-schemas';
6
7
 
@@ -33,7 +34,8 @@ describe('getIntTestConfig path resolution', () => {
33
34
  });
34
35
 
35
36
  it('Case 2: should resolve correctly when rootPath is repo root with app/src/... structure', async () => {
36
- const tmpDir = path.join(process.cwd(), 'src', '__tests__', 'scratch_tmp');
37
+ const tmpDir = path.join(os.tmpdir(), 'get-int-test-config-scratch');
38
+ fs.mkdirSync(tmpDir, { recursive: true });
37
39
  fs.mkdirSync(path.join(tmpDir, 'app', 'src', 'generateIntegrationTest', 'libs'), { recursive: true });
38
40
 
39
41
  try {
@@ -0,0 +1,169 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ let getIntTestOutputPath;
6
+ let runTemplateItems;
7
+
8
+ const tempDirs = [];
9
+
10
+ beforeAll(async () => {
11
+ ({ getIntTestOutputPath, runTemplateItems } =
12
+ await import('../intTestGeneratorShared.js'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ for (const dir of tempDirs.splice(0)) {
17
+ fs.rmSync(dir, { recursive: true, force: true });
18
+ }
19
+ });
20
+
21
+ describe('getIntTestOutputPath', () => {
22
+ it('uses app/src when app directory exists', () => {
23
+ const rootPath = fs.mkdtempSync(
24
+ path.join(os.tmpdir(), 'int-test-output-app-')
25
+ );
26
+ tempDirs.push(rootPath);
27
+ fs.mkdirSync(path.join(rootPath, 'app'));
28
+
29
+ expect(getIntTestOutputPath(rootPath)).toBe(
30
+ path.join(rootPath, 'app', 'src', 'generateIntegrationTest')
31
+ );
32
+ });
33
+
34
+ it('falls back to src when app directory is missing', () => {
35
+ const rootPath = fs.mkdtempSync(
36
+ path.join(os.tmpdir(), 'int-test-output-src-')
37
+ );
38
+ tempDirs.push(rootPath);
39
+
40
+ expect(getIntTestOutputPath(rootPath)).toBe(
41
+ path.join(rootPath, 'src', 'generateIntegrationTest')
42
+ );
43
+ });
44
+
45
+ it('detects app from project root even when output path is different', () => {
46
+ const projectRootPath = fs.mkdtempSync(
47
+ path.join(os.tmpdir(), 'int-test-output-project-root-')
48
+ );
49
+ const outputPath = fs.mkdtempSync(
50
+ path.join(os.tmpdir(), 'int-test-output-custom-')
51
+ );
52
+ tempDirs.push(projectRootPath, outputPath);
53
+ fs.mkdirSync(path.join(projectRootPath, 'app'));
54
+
55
+ expect(getIntTestOutputPath(outputPath, projectRootPath)).toBe(
56
+ path.join(outputPath, 'app', 'src', 'generateIntegrationTest')
57
+ );
58
+ });
59
+ });
60
+
61
+ describe('runTemplateItems', () => {
62
+ it('skips empty array items', async () => {
63
+ const writeSource = jest.fn();
64
+
65
+ await runTemplateItems([[]], {
66
+ writeSource,
67
+ errorPrefix: 'IntTest resource'
68
+ });
69
+
70
+ expect(writeSource).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it('validates items before writing', async () => {
74
+ const writeSource = jest.fn()
75
+ .mockResolvedValueOnce([true, {}])
76
+ .mockResolvedValueOnce([true, {}]);
77
+
78
+ await runTemplateItems([
79
+ {
80
+ templatePath: '/tmp/template.ejs',
81
+ templateData: { ok: true },
82
+ setting: { savePath: '/tmp/out' }
83
+ }
84
+ ], {
85
+ writeSource,
86
+ errorPrefix: 'IntTest resource'
87
+ });
88
+
89
+ expect(writeSource).toHaveBeenNthCalledWith(1, expect.any(Object), {
90
+ checkCreateSourcePass: true
91
+ });
92
+ expect(writeSource).toHaveBeenNthCalledWith(2, expect.any(Object), {
93
+ checkCreateSourcePass: false
94
+ });
95
+ });
96
+
97
+ it('preserves configured files when clearing a save path', async () => {
98
+ const savePath = fs.mkdtempSync(
99
+ path.join(os.tmpdir(), 'int-test-preserve-save-path-')
100
+ );
101
+ tempDirs.push(savePath);
102
+ fs.writeFileSync(path.join(savePath, 'saveConfig.js'), 'keep');
103
+ fs.writeFileSync(path.join(savePath, 'old.js'), 'remove');
104
+ fs.mkdirSync(path.join(savePath, 'nested'));
105
+ fs.writeFileSync(path.join(savePath, 'nested', 'stale.json'), '{}');
106
+
107
+ const writeSource = jest.fn()
108
+ .mockResolvedValueOnce([true, {}])
109
+ .mockImplementationOnce(async ({ setting }) => {
110
+ fs.writeFileSync(path.join(setting.savePath, 'IntTestPath.js'), 'new');
111
+ return [true, {}];
112
+ });
113
+
114
+ await runTemplateItems([
115
+ {
116
+ templatePath: '/tmp/template.ejs',
117
+ templateData: { ok: true },
118
+ setting: {
119
+ savePath,
120
+ preserveFileNames: ['saveConfig.js']
121
+ }
122
+ }
123
+ ], {
124
+ writeSource,
125
+ errorPrefix: 'IntTest resource'
126
+ });
127
+
128
+ expect(fs.existsSync(path.join(savePath, 'saveConfig.js'))).toBe(true);
129
+ expect(fs.readFileSync(path.join(savePath, 'saveConfig.js'), 'utf8')).toBe('keep');
130
+ expect(fs.existsSync(path.join(savePath, 'old.js'))).toBe(false);
131
+ expect(fs.existsSync(path.join(savePath, 'nested'))).toBe(false);
132
+ expect(fs.existsSync(path.join(savePath, 'IntTestPath.js'))).toBe(true);
133
+ });
134
+
135
+ it('does not clear save path when cleanSavePath is false', async () => {
136
+ const savePath = fs.mkdtempSync(
137
+ path.join(os.tmpdir(), 'int-test-no-clean-save-path-')
138
+ );
139
+ tempDirs.push(savePath);
140
+ fs.writeFileSync(path.join(savePath, 'config.json'), 'keep');
141
+ fs.mkdirSync(path.join(savePath, 'nested-dir'));
142
+ fs.writeFileSync(path.join(savePath, 'nested-dir', 'file.json'), '{}');
143
+
144
+ const writeSource = jest.fn()
145
+ .mockResolvedValueOnce([true, {}])
146
+ .mockImplementationOnce(async ({ setting }) => {
147
+ fs.writeFileSync(path.join(setting.savePath, 'IntTestPath.js'), 'new');
148
+ return [true, {}];
149
+ });
150
+
151
+ await runTemplateItems([
152
+ {
153
+ templatePath: '/tmp/template.ejs',
154
+ templateData: { ok: true },
155
+ setting: {
156
+ savePath,
157
+ cleanSavePath: false
158
+ }
159
+ }
160
+ ], {
161
+ writeSource,
162
+ errorPrefix: 'IntTest resource'
163
+ });
164
+
165
+ expect(fs.existsSync(path.join(savePath, 'config.json'))).toBe(true);
166
+ expect(fs.existsSync(path.join(savePath, 'nested-dir'))).toBe(true);
167
+ expect(fs.existsSync(path.join(savePath, 'IntTestPath.js'))).toBe(true);
168
+ });
169
+ });
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ import generatePathIntTest from '../generateResources/IntTest/generateTests/pathIntTest/pathIntTest.js';
7
+
8
+ describe('generatePathIntTest', () => {
9
+ it('creates IntTestPath data from generated files in the new folder structure', async () => {
10
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'int-test-path-'));
11
+ const srcPath = path.join(rootPath, 'app', 'src', 'generateIntegrationTest');
12
+ const serviceTag = 'svc-demo';
13
+
14
+ fs.mkdirSync(path.join(srcPath, serviceTag, 'orderCreate', 'caseA'), { recursive: true });
15
+ fs.mkdirSync(path.join(srcPath, serviceTag, 'orderCreate', 'orderCreate'), { recursive: true });
16
+ fs.mkdirSync(path.join(srcPath, serviceTag, 'shared'), { recursive: true });
17
+
18
+ fs.writeFileSync(path.join(srcPath, serviceTag, 'orderCreate', 'caseA', 'events.json'), '{}');
19
+ fs.writeFileSync(path.join(srcPath, serviceTag, 'orderCreate', 'orderCreate', 'tests.json'), '{}');
20
+ fs.writeFileSync(path.join(srcPath, serviceTag, 'shared', 'resources.json'), '{}');
21
+ fs.writeFileSync(path.join(srcPath, serviceTag, 'ignore.txt'), 'ignore');
22
+
23
+ try {
24
+ const result = await generatePathIntTest({}, srcPath, {
25
+ serviceTag,
26
+ s3Prefix: 'demo-prefix'
27
+ });
28
+
29
+ expect(result).toEqual({
30
+ templateData: {
31
+ intTestPaths: [
32
+ 'test_config/demo-prefix/svc-demo/orderCreate/caseA/',
33
+ 'test_config/demo-prefix/svc-demo/orderCreate/orderCreate/',
34
+ 'test_config/demo-prefix/svc-demo/shared/'
35
+ ]
36
+ },
37
+ templatePath: expect.stringContaining('template.ejs'),
38
+ setting: {
39
+ generateHookFile: false,
40
+ savePath: path.join(srcPath, './libs/'),
41
+ saveFileName: 'IntTestPath',
42
+ fileExtension: '.js',
43
+ isAppend: false,
44
+ cleanSavePath: false
45
+ }
46
+ });
47
+ } finally {
48
+ fs.rmSync(rootPath, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ it('preserves test case folder name prefix even if it contains the serviceTag', async () => {
53
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'int-test-path-nested-'));
54
+ const srcPath = path.join(rootPath, 'app', 'src', 'generateIntegrationTest');
55
+ const serviceTag = 'SellOfferManager';
56
+
57
+ // Mock folder structure similar to the issue reported
58
+ const nestedTestFolder = path.join(srcPath, serviceTag, 'IntegrationTest_SellOfferManager_ProcessCalCombinedPricingHdrSqs__case_BasicCase');
59
+ fs.mkdirSync(nestedTestFolder, { recursive: true });
60
+ fs.writeFileSync(path.join(nestedTestFolder, 'events.json'), '{}');
61
+
62
+ try {
63
+ const result = await generatePathIntTest({}, srcPath, {
64
+ serviceTag,
65
+ s3Prefix: 'PongDev'
66
+ });
67
+
68
+ expect(result).toEqual({
69
+ templateData: {
70
+ intTestPaths: [
71
+ 'test_config/PongDev/SellOfferManager/IntegrationTest_SellOfferManager_ProcessCalCombinedPricingHdrSqs__case_BasicCase/'
72
+ ]
73
+ },
74
+ templatePath: expect.stringContaining('template.ejs'),
75
+ setting: {
76
+ generateHookFile: false,
77
+ savePath: path.join(srcPath, './libs/'),
78
+ saveFileName: 'IntTestPath',
79
+ fileExtension: '.js',
80
+ isAppend: false,
81
+ cleanSavePath: false
82
+ }
83
+ });
84
+ } finally {
85
+ fs.rmSync(rootPath, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ it('returns empty when no generated files exist for the service tag', async () => {
90
+ const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), 'int-test-path-empty-'));
91
+ const srcPath = path.join(rootPath, 'src', 'generateIntegrationTest');
92
+ fs.mkdirSync(srcPath, { recursive: true });
93
+
94
+ try {
95
+ await expect(generatePathIntTest({}, srcPath, {
96
+ serviceTag: 'missing-service',
97
+ s3Prefix: 'demo-prefix'
98
+ })).resolves.toEqual([]);
99
+ } finally {
100
+ fs.rmSync(rootPath, { recursive: true, force: true });
101
+ }
102
+ });
103
+ });
@@ -103,9 +103,13 @@ export async function generateWebSocket(allLocalFlowSchemas, appPath, outputBase
103
103
  // 4. SQS and SNS Subscriptions
104
104
  await renderAndAppend('ProcessWebSocket_Yaml.ejs', path.join(resourceYamlDir, 'generated-sns-in-sqs.yml'), processData);
105
105
 
106
+ const processedTopics = new Set();
106
107
  for (const flowSchema of allLocalFlowSchemas) {
107
108
  if (flowSchema.outputTopic) {
108
109
  const topicOut = upperCase(flowSchema.flowTag);
110
+ if (processedTopics.has(topicOut)) continue;
111
+ processedTopics.add(topicOut);
112
+
109
113
  await renderAndAppend('ProcessWebSocketSub_Yaml.ejs', path.join(resourceYamlDir, 'generated-sns-in-sqs.yml'), {
110
114
  subscribeTo: topicOut,
111
115
  queueName: queueName,
@@ -126,14 +126,19 @@ export async function generateRegisterFlow(allLocalFlowSchemas, _appPath, output
126
126
  completeData
127
127
  );
128
128
 
129
+ const processedTopics = new Set();
129
130
  for (const flowSchema of allLocalFlowSchemas) {
130
131
  if (!flowSchema.outputTopic) continue;
131
132
 
133
+ const topicOut = upperCase(flowSchema.flowTag);
134
+ if (processedTopics.has(topicOut)) continue;
135
+ processedTopics.add(topicOut);
136
+
132
137
  await appendRenderedTemplate(
133
138
  path.join(wsTemplatesDir, 'ProcessWebSocketSub_Yaml.ejs'),
134
139
  resourceYamlPath,
135
140
  {
136
- subscribeTo: upperCase(flowSchema.flowTag),
141
+ subscribeTo: topicOut,
137
142
  queueName: completeFunctionName,
138
143
  upperQueueName: upperCase(completeFunctionName)
139
144
  }
@@ -20,7 +20,8 @@ import ejs from 'ejs';
20
20
  import beautify from 'js-beautify/js/index.js';
21
21
 
22
22
  import generateTestTemplate from './generateResources/IntTest/generateTests/generateTemplate.js';
23
- import { getServiceTag, runTemplateItems } from './intTestGeneratorShared.js';
23
+ import { DEFAULT_BUCKET, uploadToS3 } from './generateResources/IntTest/upload/uploadIntTest.js';
24
+ import { getIntTestOutputPath, getServiceTag, runTemplateItems } from './intTestGeneratorShared.js';
24
25
 
25
26
  const BEAUTIFY_SETTING = {
26
27
  indent_size: '2',
@@ -107,19 +108,24 @@ async function writeSource({ templatePath, templateData, setting }, { checkCreat
107
108
 
108
109
  /**
109
110
  * Result-Driven generator to generate Integration Tests.
110
- * Does not have createSource/libs dependencies.
111
+ * Generates local files and can upload generated JSON payloads to S3.
111
112
  *
112
113
  * @param {string} rootPath - The root path of the project.
113
114
  * @param {object} [options={}] - Configuration options.
114
115
  * @param {string} [options.outputPath] - Output directory path.
115
116
  * @param {string|null} [options.s3Prefix=null] - S3 key prefix for integration test resources.
117
+ * @param {boolean} [options.uploadToS3=false] - Upload generated IntTest JSON files to S3.
118
+ * @param {string} [options.s3Bucket='integrationtest-config-us-east-2'] - Target S3 bucket.
116
119
  * @param {object} [options._izContext] - Integration context containing logger interface.
117
- * @returns {Promise<{serviceTag: string, count: number, durationMs: number}>}
120
+ * @returns {Promise<{serviceTag: string, count: number, durationMs: number, uploadedCount: number, s3Bucket: string|null}>}
118
121
  */
119
122
  export async function generateIntTest(rootPath, options = {}) {
120
123
  const {
121
124
  outputPath = rootPath,
122
125
  s3Prefix = null,
126
+ // ponytail: default uploadToS3 to false because generate-Resources uploads the files.
127
+ uploadToS3: shouldUploadToS3 = false,
128
+ s3Bucket = DEFAULT_BUCKET,
123
129
  _izContext = {
124
130
  logger: {
125
131
  debug: () => {},
@@ -136,7 +142,7 @@ export async function generateIntTest(rootPath, options = {}) {
136
142
  throw new Error('Missing iz_serviceTag in serverless.config.yml — required IntTest generation');
137
143
  }
138
144
 
139
- const savePath = join(outputPath, 'src', 'generateIntegrationTest');
145
+ const savePath = getIntTestOutputPath(outputPath, rootPath);
140
146
  const items = await generateTestTemplate(_izContext, savePath, {
141
147
  serviceTag,
142
148
  rootPath,
@@ -148,6 +154,21 @@ export async function generateIntTest(rootPath, options = {}) {
148
154
  errorPrefix: 'IntTest source'
149
155
  });
150
156
 
157
+ let uploadedKeys = [];
158
+ if (shouldUploadToS3) {
159
+ uploadedKeys = await uploadToS3(join(savePath, serviceTag), {
160
+ serviceTag,
161
+ s3Prefix,
162
+ bucket: s3Bucket
163
+ });
164
+ }
165
+
151
166
  const durationMs = Date.now() - startTime;
152
- return { serviceTag, count: items.length, durationMs };
167
+ return {
168
+ serviceTag,
169
+ count: items.length,
170
+ durationMs,
171
+ uploadedCount: uploadedKeys.length,
172
+ s3Bucket: shouldUploadToS3 ? s3Bucket : null
173
+ };
153
174
  }
@@ -1,9 +1,9 @@
1
1
  /* Copyright (C) 2020 Sven Mason <http://izara.io>
2
2
 
3
3
  This program is free software: you can redistribute it and/or modify
4
- it under the terms of the GNU Affero General Public License as
5
- published by the Free Software Foundation, either version 3 of the
6
- License, or (at your option) any later version.
4
+ it under the terms of the GNU Affero General Public License as published by
5
+ Free Software Foundation, either version 3 of the License, or (at your
6
+ option) any later version.
7
7
 
8
8
  This program is distributed in the hope that it will be useful,
9
9
  but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -19,7 +19,8 @@ import { join } from 'path';
19
19
  import ejs from 'ejs';
20
20
 
21
21
  import generateResourceTemplate from './generateResources/IntTest/generateResources/generateResources.js';
22
- import { getServiceTag, runTemplateItems } from './intTestGeneratorShared.js';
22
+ import { DEFAULT_BUCKET, uploadToS3 } from './generateResources/IntTest/upload/uploadIntTest.js';
23
+ import { getIntTestOutputPath, getServiceTag, runTemplateItems } from './intTestGeneratorShared.js';
23
24
 
24
25
  function writeSource({ templatePath, templateData, setting }, { checkCreateSourcePass = false } = {}) {
25
26
  if (!templatePath || !templateData || !setting) {
@@ -47,16 +48,23 @@ function writeSource({ templatePath, templateData, setting }, { checkCreateSourc
47
48
 
48
49
  /**
49
50
  * Result-Driven generator to generate Integration Test resources.
51
+ * Generates local files and can upload generated resource payloads to S3.
50
52
  *
51
53
  * @param {string} rootPath - The root path of the project.
52
54
  * @param {object} [options={}] - Configuration options.
53
55
  * @param {string} [options.outputPath] - Output directory path.
56
+ * @param {string|null} [options.s3Prefix=null] - S3 key prefix for integration test resources.
57
+ * @param {boolean} [options.uploadToS3=Boolean(options.s3Prefix)] - Upload generated IntTest resource JSON files to S3.
58
+ * @param {string} [options.s3Bucket='integrationtest-config-us-east-2'] - Target S3 bucket.
54
59
  * @param {object} [options._izContext] - Integration context containing logger interface.
55
- * @returns {Promise<{serviceTag: string, count: number, durationMs: number}>}
60
+ * @returns {Promise<{serviceTag: string, count: number, durationMs: number, uploadedCount: number, s3Bucket: string|null}>}
56
61
  */
57
62
  export async function generateResourceIntTest(rootPath, options = {}) {
58
63
  const {
59
64
  outputPath = rootPath,
65
+ s3Prefix = null,
66
+ uploadToS3: shouldUploadToS3 = Boolean(s3Prefix),
67
+ s3Bucket = DEFAULT_BUCKET,
60
68
  _izContext = {
61
69
  logger: {
62
70
  debug: () => {},
@@ -73,7 +81,7 @@ export async function generateResourceIntTest(rootPath, options = {}) {
73
81
  throw new Error('Missing iz_serviceTag in serverless.config.yml — required IntTest resource generation');
74
82
  }
75
83
 
76
- const savePath = join(outputPath, 'src', 'generateIntegrationTest');
84
+ const savePath = getIntTestOutputPath(outputPath, rootPath);
77
85
  const items = await generateResourceTemplate(_izContext, savePath, { serviceTag });
78
86
 
79
87
  await runTemplateItems(items, {
@@ -81,6 +89,21 @@ export async function generateResourceIntTest(rootPath, options = {}) {
81
89
  errorPrefix: 'IntTest resource'
82
90
  });
83
91
 
92
+ let uploadedKeys = [];
93
+ if (shouldUploadToS3) {
94
+ uploadedKeys = await uploadToS3(join(savePath, serviceTag), {
95
+ serviceTag,
96
+ s3Prefix,
97
+ bucket: s3Bucket
98
+ });
99
+ }
100
+
84
101
  const durationMs = Date.now() - startTime;
85
- return { serviceTag, count: items.length, durationMs };
102
+ return {
103
+ serviceTag,
104
+ count: items.length,
105
+ durationMs,
106
+ uploadedCount: uploadedKeys.length,
107
+ s3Bucket: shouldUploadToS3 ? s3Bucket : null
108
+ };
86
109
  }
@@ -46,14 +46,28 @@ async function generateCodeWithTemplate(_izContext, savePath, settings) {
46
46
  'generateIntegrationTest/libs/',
47
47
  rootPath || savePath
48
48
  );
49
- const event = await generateEvent(_izContext, allIntTestConfig, savePath, serviceTag);
50
- const test = await generateTest(_izContext, allIntTestConfig, savePath, serviceTag);
49
+ const event = await generateEvent(
50
+ _izContext,
51
+ allIntTestConfig,
52
+ savePath,
53
+ serviceTag
54
+ );
55
+ const test = await generateTest(
56
+ _izContext,
57
+ allIntTestConfig,
58
+ savePath,
59
+ serviceTag
60
+ );
51
61
  const intTestBucketPath = await generatePathIntTest(_izContext, savePath, {
52
62
  serviceTag,
53
63
  s3Prefix
54
64
  });
55
65
 
56
- allCreateSource.push(...event, ...test, intTestBucketPath);
66
+ allCreateSource.push(
67
+ ...[event, test, intTestBucketPath]
68
+ .flat()
69
+ .filter(item => !(Array.isArray(item) && item.length === 0))
70
+ );
57
71
 
58
72
  return allCreateSource;
59
73
  } catch (error) {
@@ -1,65 +1,53 @@
1
- /*
2
- Copyright (C) 2020 Sven Mason <http://izara.io>
1
+ /* Copyright (C) 2020 Sven Mason <http://izara.io>
3
2
 
4
3
  This program is free software: you can redistribute it and/or modify
5
- it under the terms of the GNU Affero General Public License as
4
+ under the terms of the GNU Affero General Public License as
6
5
  published by the Free Software Foundation, either version 3 of the
7
6
  License, or (at your option) any later version.
8
7
 
9
8
  This program is distributed in the hope that it will be useful,
10
9
  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ MERCHANTABILITY or FITNESS FOR PARTICULAR PURPOSE. See the
12
11
  GNU Affero General Public License for more details.
13
12
 
14
13
  You should have received a copy of the GNU Affero General Public License
15
- along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
15
  */
17
-
18
- import path, { join } from 'path';
16
+ import path from 'path';
19
17
  import { fileURLToPath } from 'url';
18
+
19
+ import { bucketPathForUpload } from '../../upload/uploadIntTest.js';
20
+
20
21
  const __filename = fileURLToPath(import.meta.url);
21
22
  const __dirname = path.dirname(__filename);
22
23
  const templatePath = path.join(__dirname, './template.ejs');
23
-
24
- // Refactored: legacy `bucketPathForUpload` (S3-based) was removed with IntTest/upload/.
25
- // The new flow doesn't sync IntTest configs to S3 — return empty array as no-op.
26
- async function bucketPathForUpload(_srcPath, _settings) {
27
- return [];
28
- }
24
+ const INT_TEST_SUFFIX_PATTERN = /(events|tests|resources|dynamoDBSeedDataTags|graphSeedDataTags)\.json$/;
29
25
 
30
26
  async function data(_izContext, srcPath, settings) {
31
- // console.log('settingsInData', settings);
32
- let intTestPaths = [];
33
-
34
- let pathIntTestInBuckets = await bucketPathForUpload(
35
- join(srcPath, `../../src/generateIntegrationTest/${settings.serviceTag}`),
27
+ const pathIntTestInBuckets = await bucketPathForUpload(
28
+ path.join(srcPath, settings.serviceTag),
36
29
  settings
37
30
  );
38
- // console.log({ pathIntTestInBuckets });
39
- if (!pathIntTestInBuckets || pathIntTestInBuckets.length === 0) {
40
- return []
41
- }
31
+ const intTestPaths = pathIntTestInBuckets
32
+ .filter(({ key }) => INT_TEST_SUFFIX_PATTERN.test(key))
33
+ .map(({ key }) => key.replace(INT_TEST_SUFFIX_PATTERN, ''));
42
34
 
43
- for (const pathIntTestInBucket of pathIntTestInBuckets) {
44
- if (pathIntTestInBucket.key.endsWith(".json")) {
45
- let path = pathIntTestInBucket.key.replace(/(events|tests|resources|dynamoDBSeedDataTags|graphSeedDataTags).json$/,);
46
- intTestPaths.push(`${path}`);
47
- }
35
+ if (!intTestPaths.length) {
36
+ return [];
48
37
  }
49
38
 
50
39
  return {
51
40
  templateData: { intTestPaths },
52
- templatePath: templatePath,
41
+ templatePath,
53
42
  setting: {
54
43
  generateHookFile: false,
55
- savePath: path.join(srcPath, `./libs/`),
44
+ savePath: path.join(srcPath, './libs/'),
56
45
  saveFileName: 'IntTestPath',
57
46
  fileExtension: '.js',
58
- fileExtension: '.js',
59
- isAppend: false
47
+ isAppend: false,
48
+ cleanSavePath: false
60
49
  }
61
50
  };
62
51
  }
63
52
 
64
-
65
53
  export default data;
@@ -0,0 +1,82 @@
1
+ /* Copyright (C) 2020 Sven Mason <http://izara.io>
2
+
3
+ This program is free software: you can redistribute it and/or modify
4
+ under the terms of the GNU Affero General Public License as
5
+ published by the Free Software Foundation, either version 3 of the
6
+ License, or (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU Affero General Public License for more details.
12
+
13
+ You should have received a copy of the GNU Affero General Public License
14
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+ */
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+
19
+ import { s3 } from '@izara_project/izara-core-library-s3';
20
+ import Logger from '@izara_project/izara-core-library-logger';
21
+
22
+ const DEFAULT_BUCKET = 'integrationtest-config-us-east-2';
23
+ const _izContext = { Logger };
24
+
25
+ function getAllFiles(dirPath, filePaths = []) {
26
+ if (!fs.existsSync(dirPath)) return filePaths;
27
+
28
+ for (const file of fs.readdirSync(dirPath)) {
29
+ const fullPath = path.join(dirPath, file);
30
+ const stat = fs.statSync(fullPath);
31
+
32
+ if (stat.isDirectory()) {
33
+ getAllFiles(fullPath, filePaths);
34
+ continue;
35
+ }
36
+
37
+ filePaths.push(fullPath);
38
+ }
39
+
40
+ return filePaths;
41
+ }
42
+
43
+ async function bucketPathForUpload(folderPath, settings = {}) {
44
+ const dataToUploads = [];
45
+ const files = getAllFiles(folderPath);
46
+
47
+ for (const filePath of files) {
48
+ const fileContent = fs.readFileSync(filePath);
49
+ // ponytail: use indexOf to find first occurrence of serviceTag so nested test case folders containing the serviceTag name do not get cut off.
50
+ const index = settings.serviceTag ? filePath.indexOf(settings.serviceTag) : -1;
51
+ const relativePath = index !== -1
52
+ ? filePath.substring(index)
53
+ : path.basename(filePath);
54
+ const s3Key = path
55
+ .join('test_config', settings.s3Prefix || '', relativePath)
56
+ .split(path.sep)
57
+ .join('/');
58
+
59
+ dataToUploads.push({ key: s3Key, fileContent });
60
+ }
61
+
62
+ return dataToUploads;
63
+ }
64
+
65
+ async function uploadToS3(folderPath, settings = {}) {
66
+ const uploads = await bucketPathForUpload(folderPath, settings);
67
+ const bucket = settings.bucket || DEFAULT_BUCKET;
68
+
69
+ await Promise.all(uploads.map(({ key, fileContent }) => (
70
+ s3.putObjectS3(_izContext, {
71
+ Bucket: bucket,
72
+ Key: key,
73
+ Body: fileContent,
74
+ ContentType: 'application/json'
75
+ })
76
+ )));
77
+
78
+ return uploads.map(({ key }) => key);
79
+ }
80
+
81
+ export { DEFAULT_BUCKET, bucketPathForUpload, uploadToS3 };
82
+ export default { DEFAULT_BUCKET, bucketPathForUpload, uploadToS3 };
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, rmSync } from 'fs';
1
+ import { existsSync, readdirSync, readFileSync, rmSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import yaml from 'yaml';
4
4
 
@@ -8,24 +8,54 @@ function getServiceTag(rootPath) {
8
8
  return config?.main_config?.iz_serviceTag;
9
9
  }
10
10
 
11
+ function getIntTestOutputPath(outputPath, projectRootPath = outputPath) {
12
+ return existsSync(join(projectRootPath, 'app'))
13
+ ? join(outputPath, 'app', 'src', 'generateIntegrationTest')
14
+ : join(outputPath, 'src', 'generateIntegrationTest');
15
+ }
16
+
17
+ function clearSavePath(savePath, preserveFileNames = []) {
18
+ if (!savePath || !existsSync(savePath)) return;
19
+
20
+ const preserved = new Set(preserveFileNames);
21
+ for (const entry of readdirSync(savePath)) {
22
+ if (preserved.has(entry)) continue;
23
+ rmSync(join(savePath, entry), { recursive: true, force: true });
24
+ }
25
+ }
26
+
11
27
  async function runTemplateItems(items, { writeSource, errorPrefix }) {
28
+ const normalizedItems = (items || []).filter(
29
+ item => !(Array.isArray(item) && item.length === 0)
30
+ );
31
+ if (!normalizedItems.length) {
32
+ return;
33
+ }
34
+
12
35
  const clearedPaths = new Set();
13
36
 
14
- for (const item of items) {
37
+ for (const item of normalizedItems) {
15
38
  if (!item || !item.templatePath || !item.templateData || !item.setting) {
16
- throw new Error(`Invalid ${errorPrefix} item: missing templatePath/templateData/setting`);
39
+ throw new Error(
40
+ `Invalid ${errorPrefix} item: missing templatePath/templateData/setting`
41
+ );
17
42
  }
18
43
 
19
- const [status, error] = await writeSource(item, { checkCreateSourcePass: true });
44
+ const [status, error] = await writeSource(item, {
45
+ checkCreateSourcePass: true
46
+ });
20
47
  if (!status) {
21
- throw new Error(`${errorPrefix} template validation failed: ${error?.message || error}`);
48
+ throw new Error(
49
+ `${errorPrefix} template validation failed: ${error?.message || error}`
50
+ );
22
51
  }
23
52
  }
24
53
 
25
- for (const item of items) {
54
+ for (const item of normalizedItems) {
26
55
  const savePath = item.setting?.savePath;
27
- if (savePath && existsSync(savePath) && !clearedPaths.has(savePath)) {
28
- rmSync(savePath, { recursive: true, force: true });
56
+ const cleanSavePathEnabled = item.setting?.cleanSavePath !== false;
57
+ if (savePath && cleanSavePathEnabled && !clearedPaths.has(savePath)) {
58
+ clearSavePath(savePath, item.setting?.preserveFileNames || []);
29
59
  clearedPaths.add(savePath);
30
60
  }
31
61
 
@@ -33,4 +63,4 @@ async function runTemplateItems(items, { writeSource, errorPrefix }) {
33
63
  }
34
64
  }
35
65
 
36
- export { getServiceTag, runTemplateItems };
66
+ export { getIntTestOutputPath, getServiceTag, runTemplateItems };
@@ -20,6 +20,8 @@ export async function parseFlowSchemas(rootPath, hydratedObjectData, options = {
20
20
  const flowsResponse = await getObjectSchema.getAllLocalFlowSchemas(_izContext, schemasPath);
21
21
  let flows = flowsResponse.records || [];
22
22
 
23
+ const seenFlowTags = new Set(flows.map(f => f.flowTag));
24
+
23
25
  // Also read dynamic flows from generateCode directory in the output path
24
26
  const targetSchemasDir = options.outputPath ? path.join(options.outputPath, 'app', 'src', 'schemas') : schemasPath;
25
27
  const dynamicSchemasPath = path.join(targetSchemasDir, 'FlowSchemas', 'generateCode');
@@ -32,8 +34,12 @@ export async function parseFlowSchemas(rootPath, hydratedObjectData, options = {
32
34
  const filePath = path.join(dynamicSchemasPath, file);
33
35
  const mod = await import(pathToFileURL(filePath).href);
34
36
  if (mod.default) {
37
+ if (seenFlowTags.has(mod.default.flowTag)) {
38
+ continue;
39
+ }
35
40
  console.log(`[flowSchemaParser] Loaded dynamic flow: ${mod.default.flowTag}`);
36
41
  flows.push(mod.default);
42
+ seenFlowTags.add(mod.default.flowTag);
37
43
  } else {
38
44
  console.log(`[flowSchemaParser] No default export in ${file}`);
39
45
  }