diff --git a/dashboard.json b/dashboard.json index a2b37a65dc79..8ecaa6a03cd6 100644 --- a/dashboard.json +++ b/dashboard.json @@ -17,24 +17,15 @@ "score": 34.46095064991105 }, { - "id": "I_kwDODou01c5BZZv-", - "isPR": false, + "id": "PR_kwDOBW5R_c5-T7mG", + "isPR": true, "isAssigned": false, - "title": "Open Graph link preview image according to the document to open", - "author": "smoya", - "resourcePath": "/asyncapi/studio/issues/224", - "repo": "asyncapi/studio", - "labels": [ - { - "name": "enhancement", - "color": "a2eeef" - }, - { - "name": "keep-open", - "color": "f9dd4b" - } - ], - "score": 31.302030173669205 + "title": "feat: add tests for build post list script", + "author": "vishvamsinh28", + "resourcePath": "/asyncapi/website/pull/3284", + "repo": "asyncapi/website", + "labels": [], + "score": 29.866157229922912 }, { "id": "I_kwDOGQYLdM5AX1lK", @@ -54,29 +45,18 @@ "color": "0E8A16" } ], - "score": 28.430284286176615 - }, - { - "id": "PR_kwDOBW5R_c5-T7mG", - "isPR": true, - "isAssigned": false, - "title": "feat: add tests for build post list script", - "author": "vishvamsinh28", - "resourcePath": "/asyncapi/website/pull/3284", - "repo": "asyncapi/website", - "labels": [], - "score": 27.28158593117958 + "score": 28.717458874925875 }, { - "id": "PR_kwDOBW5R_c59FBoR", + "id": "PR_kwDOBW5R_c6BZLuT", "isPR": true, "isAssigned": false, - "title": "feat: add tests for tool-object script", + "title": "feat: add tests for check markdown script", "author": "vishvamsinh28", - "resourcePath": "/asyncapi/website/pull/3265", + "resourcePath": "/asyncapi/website/pull/3378", "repo": "asyncapi/website", "labels": [], - "score": 25.845712987433288 + "score": 23.26114168868996 }, { "id": "PR_kwDOFLhIt85bqKL8", @@ -121,17 +101,6 @@ ], "score": 21.825268744943667 }, - { - "id": "PR_kwDOBW5R_c6BZLuT", - "isPR": true, - "isAssigned": false, - "title": "feat: add tests for check markdown script", - "author": "vishvamsinh28", - "resourcePath": "/asyncapi/website/pull/3378", - "repo": "asyncapi/website", - "labels": [], - "score": 20.67657038994663 - }, { "id": "PR_kwDOFLhIt855u7Eb", "isPR": true, @@ -143,22 +112,6 @@ "labels": [], "score": 20.102221212448114 }, - { - "id": "PR_kwDOBW5R_c535wDj", - "isPR": true, - "isAssigned": false, - "title": "feat: add test for combine tools script", - "author": "vishvamsinh28", - "resourcePath": "/asyncapi/website/pull/3136", - "repo": "asyncapi/website", - "labels": [ - { - "name": "gsoc", - "color": "F4D03F" - } - ], - "score": 17.804824502454043 - }, { "id": "I_kwDOBW5R_c5RVOOY", "isPR": false, @@ -189,9 +142,75 @@ "repo": "asyncapi/community", "labels": [], "score": 16.08177696995849 + }, + { + "id": "I_kwDOFLhIt85bebeO", + "isPR": false, + "isAssigned": false, + "title": "Meeting Banners Storage", + "author": "AceTheCreator", + "resourcePath": "/asyncapi/community/issues/568", + "repo": "asyncapi/community", + "labels": [], + "score": 15.794602381209232 + }, + { + "id": "I_kwDODou01c5BZZv-", + "isPR": false, + "isAssigned": false, + "title": "Open Graph link preview image according to the document to open", + "author": "smoya", + "resourcePath": "/asyncapi/studio/issues/224", + "repo": "asyncapi/studio", + "labels": [ + { + "name": "enhancement", + "color": "a2eeef" + }, + { + "name": "keep-open", + "color": "f9dd4b" + } + ], + "score": 15.087185824232376 + }, + { + "id": "I_kwDODou01c5ZAFWh", + "isPR": false, + "isAssigned": true, + "title": "Please support File References", + "author": "philCryoport", + "resourcePath": "/asyncapi/studio/issues/528", + "repo": "asyncapi/studio", + "labels": [ + { + "name": "enhancement", + "color": "a2eeef" + }, + { + "name": "bounty", + "color": "0E8A16" + } + ], + "score": 14.07155484871368 } ], "goodFirstIssues": [ + { + "id": "I_kwDOFLhIt86ihL5I", + "title": "[BUG] voting summary has some errors in `isVotedInLast3Months`", + "isAssigned": false, + "resourcePath": "/asyncapi/community/issues/1614", + "repo": "asyncapi/community", + "author": "derberg", + "area": "javascript", + "labels": [ + { + "name": "bug", + "color": "d73a4a" + } + ] + }, { "id": "I_kwDOFLhIt86hMsxx", "title": "Create an announcement design for the new TSC member (Ashmit Jagtap)", @@ -608,6 +627,10 @@ { "name": "enhancement", "color": "a2eeef" + }, + { + "name": "stale", + "color": "ededed" } ] }, @@ -664,21 +687,6 @@ } ] }, - { - "id": "I_kwDOBGu-185qGt6A", - "title": "Ensure consistency when using either `Application` or `API` terms", - "isAssigned": false, - "resourcePath": "/asyncapi/spec/issues/949", - "repo": "asyncapi/spec", - "author": "smoya", - "area": "Unknown", - "labels": [ - { - "name": "enhancement", - "color": "a2eeef" - } - ] - }, { "id": "I_kwDOE8Qh385m4AtC", "title": "C# generator add xml docs from the async api description", diff --git a/scripts/tools/combine-tools.js b/scripts/tools/combine-tools.js index 602262428fa1..1b1367b15ccb 100644 --- a/scripts/tools/combine-tools.js +++ b/scripts/tools/combine-tools.js @@ -106,37 +106,44 @@ 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({ + message: 'Tool validation failed', + tool: tool.title, + source: 'manual-tools.json', + errors: validate.errors, + note: 'Script continues execution, error logged for investigation' + }); + } } } + 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 000000000000..452764ac9192 --- /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 000000000000..1184da03e718 --- /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 000000000000..47469bc1a7e7 --- /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 000000000000..622067a57462 --- /dev/null +++ b/tests/tools/combine-tools.test.js @@ -0,0 +1,229 @@ +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 { message, tool, source, note } = console.error.mock.calls[0][0]; + + expect(message).toBe('Tool validation failed'); + expect(tool).toBe('Invalid Tool'); + expect(source).toBe('manual-tools.json'); + expect(note).toBe('Script continues execution, error logged for investigation'); + + 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 () => { + const invalidPath = 'this/is/not/valid'; + await expect(combineTools(automatedTools, manualTools, invalidPath, invalidPath)) + .rejects.toThrow(/ENOENT|EACCES/); + }); + + it('should throw an error when there is an invalid category', async () => { + await expect(combineTools(invalidAutomatedToolsT10, manualTools, toolsPath, tagsPath)) + .rejects.toThrow('Error combining tools'); + }); + + it('should throw an error when URL parsing fails', async () => { + await expect(combineTools(automatedTools, manualToolsWithInvalidURLT11, toolsPath, tagsPath)) + .rejects.toThrow('Invalid URL'); + }); + + it('should handle errors when processing tools with circular references', async () => { + circularTool.circular = circularTool; + await expect(combineTools(automatedToolsT12, {}, toolsPath, tagsPath)) + .rejects.toThrow('Converting circular structure to JSON'); + }); + +});