diff --git a/scripts/tools/combine-tools.js b/scripts/tools/combine-tools.js index 602262428fa..bb290d5166c 100644 --- a/scripts/tools/combine-tools.js +++ b/scripts/tools/combine-tools.js @@ -106,37 +106,41 @@ const getFinalTool = async (toolObject) => { // Combine the automated tools and manual tools list into single JSON object file, and // lists down all the language and technology tags in one JSON file. const combineTools = async (automatedTools, manualTools, toolsPath, tagsPath) => { - for (const key in automatedTools) { - let finalToolsList = []; - if (automatedTools[key].toolsList.length) { - for (const tool of automatedTools[key].toolsList) { - finalToolsList.push(await getFinalTool(tool)) + try { + for (const key in automatedTools) { + let finalToolsList = []; + if (automatedTools[key].toolsList.length) { + for (const tool of automatedTools[key].toolsList) { + finalToolsList.push(await getFinalTool(tool)) + } } - } - if (manualTools[key] && manualTools[key].toolsList.length) { - for (const tool of manualTools[key].toolsList) { - let isAsyncAPIrepo; - const isValid = await validate(tool) - if (isValid) { - if (tool?.links?.repoUrl) { - const url = new URL(tool.links.repoUrl) - isAsyncAPIrepo = url.href.startsWith("https://github.com/asyncapi/") - } else isAsyncAPIrepo = false - let toolObject = await createToolObject(tool, "", "", isAsyncAPIrepo) - finalToolsList.push(await getFinalTool(toolObject)) - } else { - console.error('Script is not failing, it is just dropping errors for further investigation'); - console.error(`Invalid ${tool.title} .asyncapi-tool file.`); - console.error(`Located in manual-tools.json file`); - console.error('Validation errors:', JSON.stringify(validate.errors, null, 2)); + if (manualTools[key]?.toolsList?.length) { + for (const tool of manualTools[key].toolsList) { + let isAsyncAPIrepo; + const isValid = await validate(tool) + if (isValid) { + if (tool?.links?.repoUrl) { + const url = new URL(tool.links.repoUrl) + isAsyncAPIrepo = url.href.startsWith("https://github.com/asyncapi/") + } else isAsyncAPIrepo = false + let toolObject = await createToolObject(tool, "", "", isAsyncAPIrepo) + finalToolsList.push(await getFinalTool(toolObject)) + } else { + console.error('Script is not failing, it is just dropping errors for further investigation'); + console.error(`Invalid ${tool.title} .asyncapi-tool file.`); + console.error(`Located in manual-tools.json file`); + console.error('Validation errors:', JSON.stringify(validate.errors, null, 2)); + } } } + finalToolsList.sort((tool, anotherTool) => tool.title.localeCompare(anotherTool.title)); + finalTools[key].toolsList = finalToolsList } - finalToolsList.sort((tool, anotherTool) => tool.title.localeCompare(anotherTool.title)); - finalTools[key].toolsList = finalToolsList + fs.writeFileSync(toolsPath, JSON.stringify(finalTools)); + fs.writeFileSync(tagsPath, JSON.stringify({ languages: languageList, technologies: technologyList }),) + } catch (err) { + throw new Error(`Error combining tools: ${err}`); } - fs.writeFileSync(toolsPath,JSON.stringify(finalTools)); - fs.writeFileSync(tagsPath,JSON.stringify({ languages: languageList, technologies: technologyList }),) } -module.exports = { combineTools } \ No newline at end of file +module.exports = { combineTools } diff --git a/tests/fixtures/combineToolsData.js b/tests/fixtures/combineToolsData.js new file mode 100644 index 00000000000..04c776e7fee --- /dev/null +++ b/tests/fixtures/combineToolsData.js @@ -0,0 +1,212 @@ +const expectedDataT1 = { + languages: [ + { + name: 'JavaScript', + color: 'bg-[#57f281]', + borderColor: 'border-[#37f069]' + }, + { + name: 'Python', + color: 'bg-[#3572A5]', + borderColor: 'border-[#3572A5]' + } + ], + technologies: [ + { + name: 'Node.js', + color: 'bg-[#61d0f2]', + borderColor: 'border-[#40ccf7]' + }, + { + name: 'Flask', + color: 'bg-[#000000]', + borderColor: 'border-[#FFFFFF]' + } + ] +}; + +const manualToolsWithMissingData = [ + { + title: 'Tool C', + filters: {}, + links: { repoUrl: 'https://github.com/asyncapi/tool-c' } + } +]; + +const manualToolsToSort = { + category1: { + description: 'Sample Category', + toolsList: [ + { + title: 'Tool Z', + filters: { language: 'JavaScript' }, + links: { repoUrl: 'https://github.com/asyncapi/tool-z' } + }, + { + title: 'Tool A', + filters: { language: 'Python' }, + links: { repoUrl: 'https://github.com/asyncapi/tool-a' } + } + ] + } +}; + +const toolWithMultipleLanguages = { + title: 'Multi-Language Tool', + filters: { + language: ['JavaScript', 'Python', 'NewLanguage'], + technology: ['Node.js'] + }, + links: { repoUrl: 'https://github.com/example/multi-language-tool' } +}; + +const automatedToolsT5 = { + 'category1': { + description: 'Category 1 Description', + toolsList: [toolWithMultipleLanguages] + } +}; + +const invalidToolT4 = { title: 'Invalid Tool' }; + +const automatedToolsT4 = { + 'category1': { + description: 'Category 1 Description', + toolsList: [] + } +}; +const manualToolsT4 = { + 'category1': { + toolsList: [invalidToolT4] + } +}; + +const toolWithNewTagsT6 = { + title: 'New Tags Tool', + filters: { + language: 'NewLanguage', + technology: ['NewTechnology'] + }, + links: { repoUrl: 'https://github.com/example/new-tags-tool' } +}; + +const automatedToolsT6 = { + 'category1': { + description: 'Category 1 Description', + toolsList: [toolWithNewTagsT6] + } +}; + +const toolWithNewLanguageT7 = { + title: 'New Language Tool', + filters: { + language: 'Go', + technology: ['Node.js'] + }, + links: { repoUrl: 'https://github.com/example/new-language-tool' } +}; + +const automatedToolsT7 = { + 'category1': { + description: 'Category 1 Description', + toolsList: [toolWithNewLanguageT7] + } +}; + +const validToolT8 = { + title: 'Valid Tool', + filters: { + language: 'JavaScript', + technology: ['Node.js'] + }, + links: { repoUrl: 'https://github.com/asyncapi/valid-tool' } +}; + +const automatedToolsT8 = { + category1: { + description: 'Category 1 Description', + toolsList: [] + } +}; + +const manualToolsT8 = { + category1: { + toolsList: [validToolT8] + } +}; + +const toolWithoutRepoUrlT9 = { + title: 'Tool Without Repo', + filters: { + language: 'Python', + technology: ['Flask'] + }, + links: {} +}; + +const automatedToolsT9 = { + category1: { + description: 'Category 1 Description', + toolsList: [] + } +}; + +const manualToolsT9 = { + category1: { + toolsList: [toolWithoutRepoUrlT9] + } +}; + +const invalidAutomatedToolsT10 = { + invalidCategory: { + description: 'Invalid Category Description', + toolsList: [] + } +}; + +const manualToolsWithInvalidURLT11 = { + category1: { + toolsList: [ + { + title: 'Tool with Invalid URL', + filters: { language: 'JavaScript' }, + links: { repoUrl: 'invalid-url' } + } + ] + } +}; + +const circularTool = { + title: 'Circular Tool', + filters: { + language: 'JavaScript', + technology: ['Node.js'] + }, + links: { repoUrl: 'https://github.com/asyncapi/circular-tool' } +}; + +const automatedToolsT12 = { + category1: { + description: 'Category 1', + toolsList: [circularTool] + } +}; + +module.exports = { + expectedDataT1, + manualToolsWithMissingData, + manualToolsToSort, + automatedToolsT5, + automatedToolsT4, + manualToolsT4, + automatedToolsT6, + automatedToolsT7, + automatedToolsT8, + manualToolsT8, + automatedToolsT9, + manualToolsT9, + circularTool, + automatedToolsT12, + invalidAutomatedToolsT10, + manualToolsWithInvalidURLT11 +} diff --git a/tests/fixtures/tools/automated-tools.json b/tests/fixtures/tools/automated-tools.json new file mode 100644 index 00000000000..1184da03e71 --- /dev/null +++ b/tests/fixtures/tools/automated-tools.json @@ -0,0 +1,17 @@ +{ + "category1": { + "description": "Sample Category", + "toolsList": [ + { + "title": "Tool B", + "filters": { + "language": "Python", + "technology": ["Flask"] + }, + "links": { + "repoUrl": "https://github.com/asyncapi/tool-b" + } + } + ] + } +} diff --git a/tests/fixtures/tools/manual-tools.json b/tests/fixtures/tools/manual-tools.json new file mode 100644 index 00000000000..47469bc1a7e --- /dev/null +++ b/tests/fixtures/tools/manual-tools.json @@ -0,0 +1,12 @@ +[ + { + "title": "Tool A", + "filters": { + "language": "JavaScript", + "technology": ["Node.js"] + }, + "links": { + "repoUrl": "https://github.com/asyncapi/tool-a" + } + } +] diff --git a/tests/tools/combine-tools.test.js b/tests/tools/combine-tools.test.js new file mode 100644 index 00000000000..25636307459 --- /dev/null +++ b/tests/tools/combine-tools.test.js @@ -0,0 +1,265 @@ +const fs = require('fs'); +const path = require('path'); +const { combineTools } = require('../../scripts/tools/combine-tools'); +const { + expectedDataT1, + manualToolsWithMissingData, + manualToolsToSort, + automatedToolsT5, + automatedToolsT4, + manualToolsT4, + automatedToolsT6, + automatedToolsT7, + automatedToolsT8, + manualToolsT8, + automatedToolsT9, + manualToolsT9, + automatedToolsT12, + invalidAutomatedToolsT10, + manualToolsWithInvalidURLT11, + circularTool +} = require('../fixtures/combineToolsData'); + +jest.mock('ajv', () => { + return jest.fn().mockImplementation(() => ({ + compile: jest.fn().mockImplementation(() => (data) => data.title !== 'Invalid Tool'), + })); +}); + + +jest.mock('ajv-formats', () => { + return jest.fn(); +}); + +jest.mock('../../scripts/tools/tags-color', () => ({ + languagesColor: [ + { name: 'JavaScript', color: 'bg-[#57f281]', borderColor: 'border-[#37f069]' }, + { name: 'Python', color: 'bg-[#3572A5]', borderColor: 'border-[#3572A5]' } + ], + technologiesColor: [ + { name: 'Node.js', color: 'bg-[#61d0f2]', borderColor: 'border-[#40ccf7]' }, + { name: 'Flask', color: 'bg-[#000000]', borderColor: 'border-[#FFFFFF]' } + ] +})); + +jest.mock('../../scripts/tools/categorylist', () => ({ + categoryList: [ + { name: 'category1', description: 'Sample Category 1' }, + { name: 'category2', description: 'Sample Category 2' } + ] +})); + +const readJSON = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8')); + +describe('combineTools function', () => { + const toolsPath = path.join(__dirname, '../', 'fixtures', 'tools', 'tools.json'); + const tagsPath = path.join(__dirname, '../', 'fixtures', 'tools', 'tags.json'); + const manualToolsPath = path.join(__dirname, '../', 'fixtures', 'tools', 'manual-tools.json'); + const automatedToolsPath = path.join(__dirname, '../', 'fixtures', 'tools', 'automated-tools.json'); + + let manualTools; + let automatedTools; + let consoleErrorMock; + + beforeAll(() => { + manualTools = readJSON(manualToolsPath); + automatedTools = readJSON(automatedToolsPath); + }); + + afterAll(() => { + if (fs.existsSync(toolsPath)) fs.unlinkSync(toolsPath); + if (fs.existsSync(tagsPath)) fs.unlinkSync(tagsPath); + + consoleErrorMock.mockRestore(); + }); + + beforeEach(() => { + consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(() => { }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should combine tools and create correct JSON files', async () => { + await combineTools(automatedTools, manualTools, toolsPath, tagsPath); + + const combinedTools = readJSON(toolsPath); + expect(combinedTools).toHaveProperty('category1'); + + const tagsData = readJSON(tagsPath); + expect(tagsData).toHaveProperty('languages'); + expect(tagsData).toHaveProperty('technologies'); + expect(tagsData).toEqual(expectedDataT1) + }); + + it('should handle tools with missing language or technology', async () => { + + await combineTools({}, manualToolsWithMissingData, toolsPath, tagsPath); + + const combinedTools = readJSON(toolsPath); + expect(combinedTools).toHaveProperty('category1'); + }); + + it('should sort tools alphabetically by title', async () => { + + await combineTools(manualToolsToSort, {}, toolsPath, tagsPath); + + const combinedTools = readJSON(toolsPath); + const toolTitles = combinedTools.category1.toolsList.map(tool => tool.title); + expect(toolTitles).toEqual(['Tool A', 'Tool Z']); + }); + + it('should log validation errors to console.error', async () => { + + await combineTools(automatedToolsT4, manualToolsT4, toolsPath, tagsPath); + + const errorCalls = console.error.mock.calls; + + expect(errorCalls[0][0]).toBe('Script is not failing, it is just dropping errors for further investigation'); + expect(errorCalls[1][0]).toBe('Invalid Invalid Tool .asyncapi-tool file.'); + expect(errorCalls[2][0]).toBe('Located in manual-tools.json file'); + expect(errorCalls[3][0]).toEqual(expect.stringContaining('Validation errors:')); + + expect(fs.existsSync(toolsPath)).toBe(true); + expect(fs.existsSync(tagsPath)).toBe(true); + }); + + it('should handle tools with multiple languages, including new ones', async () => { + + await combineTools(automatedToolsT5, {}, toolsPath, tagsPath); + + const combinedTools = readJSON(toolsPath); + const tool = combinedTools.category1.toolsList[0]; + + expect(tool.filters.language).toHaveLength(3); + expect(tool.filters.language).toContainEqual(expect.objectContaining({ name: 'JavaScript' })); + expect(tool.filters.language).toContainEqual(expect.objectContaining({ name: 'Python' })); + expect(tool.filters.language).toContainEqual(expect.objectContaining({ name: 'NewLanguage' })); + + const tagsData = readJSON(tagsPath); + expect(tagsData.languages).toContainEqual(expect.objectContaining({ name: 'NewLanguage' })); + }); + + it('should add a new language and technology when not found in the existing lists', async () => { + + await combineTools(automatedToolsT6, {}, toolsPath, tagsPath); + + const combinedTools = readJSON(toolsPath); + const tool = combinedTools.category1.toolsList[0]; + + expect(tool.filters.language).toHaveLength(1); + expect(tool.filters.language).toContainEqual(expect.objectContaining({ name: 'NewLanguage' })); + + expect(tool.filters.technology).toHaveLength(1); + expect(tool.filters.technology).toContainEqual(expect.objectContaining({ name: 'NewTechnology' })); + + const tagsData = readJSON(tagsPath); + expect(tagsData.languages).toContainEqual({ + name: 'NewLanguage', + color: 'bg-[#57f281]', + borderColor: 'border-[#37f069]' + }); + expect(tagsData.technologies).toContainEqual({ + name: 'NewTechnology', + color: 'bg-[#61d0f2]', + borderColor: 'border-[#40ccf7]' + }); + }); + + it('should add a new language when it is not found in the existing languages list', async () => { + + await combineTools(automatedToolsT7, {}, toolsPath, tagsPath); + + const combinedTools = readJSON(toolsPath); + const tool = combinedTools.category1.toolsList[0]; + + expect(tool.filters.language).toHaveLength(1); + expect(tool.filters.language).toContainEqual(expect.objectContaining({ name: 'Go' })); + + const tagsData = readJSON(tagsPath); + expect(tagsData.languages).toContainEqual({ + name: 'Go', + color: 'bg-[#57f281]', + borderColor: 'border-[#37f069]' + }); + }); + + it('should handle valid tool objects', async () => { + + await combineTools(automatedToolsT8, manualToolsT8, toolsPath, tagsPath); + + + const tagsData = readJSON(tagsPath); + expect(tagsData.languages).toContainEqual({ + name: 'JavaScript', + color: 'bg-[#57f281]', + borderColor: 'border-[#37f069]' + }); + expect(tagsData.technologies).toContainEqual({ + name: 'Node.js', + color: 'bg-[#61d0f2]', + borderColor: 'border-[#40ccf7]' + }); + }); + + it('should handle tool objects without repoUrl', async () => { + + await combineTools(automatedToolsT9, manualToolsT9, toolsPath, tagsPath); + + const combinedTools = readJSON(toolsPath); + const tool = combinedTools.category1.toolsList[0]; + + expect(tool.isAsyncAPIrepo).toBeUndefined(); + }); + + it('should throw an error when fs.writeFileSync fails', async () => { + let error; + const invalidPath = "this/is/not/valid" + + try { + await combineTools(automatedTools, manualTools, invalidPath, invalidPath); + } catch (err) { + error = err; + expect(err.message).toMatch(/ENOENT|EACCES/); + } + expect(error).toBeDefined(); + }); + + it('should throw an error when there is an invalid category', async () => { + let error; + + try { + await combineTools(invalidAutomatedToolsT10, manualTools, toolsPath, tagsPath); + } catch (err) { + error = err; + expect(err.message).toContain('Error combining tools'); + } + expect(error).toBeDefined(); + }); + + it('should throw an error when URL parsing fails', async () => { + let error; + + try { + await combineTools(automatedTools, manualToolsWithInvalidURLT11, toolsPath, tagsPath); + } catch (err) { + error = err; + expect(err.message).toContain('Invalid URL'); + } + expect(error).toBeDefined(); + }); + + it('should handle errors when processing tools with circular references', async () => { + let error; + circularTool.circular = circularTool; + + try { + await combineTools(automatedToolsT12, {}, toolsPath, tagsPath); + } catch (err) { + error = err; + expect(err.message).toContain('Converting circular structure to JSON'); + } + expect(error).toBeDefined(); + }); +});