From 776b31d6304d9441c1863dd5e8c30039fb2f88d0 Mon Sep 17 00:00:00 2001 From: Luv Saxena Date: Tue, 20 Aug 2024 09:12:14 -0500 Subject: [PATCH] fix: add or update fields in the existing environment or rulesets (#665) * feat: initial commit * fix: merge deep code * fix: lint * fix: modification conditions * fix: simplify conditions --------- Co-authored-by: ls07667 --- lib/mergeDeep.js | 38 +- test/unit/lib/mergeDeep.test.js | 1060 ++++++++++++++++++++----------- 2 files changed, 707 insertions(+), 391 deletions(-) diff --git a/lib/mergeDeep.js b/lib/mergeDeep.js index d47fd5ce..a685a048 100644 --- a/lib/mergeDeep.js +++ b/lib/mergeDeep.js @@ -211,18 +211,19 @@ class MergeDeep { } } const combined = [] - for (const fields of Object.keys(visited)) { - combined.push(visited[fields]) - } - // Elements that are not in target are additions - additions[key] = combined.filter(item => { - if (this.isObjectNotArray(item)) { - return !target.some(targetItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(targetItem)) - } else { - return !target.includes(item) + if (Object.keys(visited).length !== 0) { + for (const fields of Object.keys(visited)) { + combined.push(visited[fields]) } - }) - + // Elements that are not in target are additions + additions[key] = combined.filter(item => { + if (this.isObjectNotArray(item)) { + return !target.some(targetItem => GET_NAME_USERNAME_PROPERTY(item) === GET_NAME_USERNAME_PROPERTY(targetItem)) + } else { + return !target.includes(item) + } + }) + } // Elements that not in source are deletions if (combined.length > 0) { // Elements that not in source are deletions @@ -247,8 +248,21 @@ class MergeDeep { this.compareDeep(a, visited[id], additions[additions.length - 1], modifications[modifications.length - 1], deletions[deletions.length - 1]) } // Any addtions for the matching key must be moved to modifications + const lastAddition = additions[additions.length - 1] + const lastModification = modifications[modifications.length - 1] + if (!this.isEmpty(additions)) { - modifications = modifications.concat(additions) + for (const key in lastAddition) { + if (!lastModification[key]) { + lastModification[key] = Array.isArray(lastAddition[key]) ? [] : {} + } + if (!Array.isArray(lastAddition[key])) { + Object.assign(lastModification[key], lastAddition[key]) + } else { + lastModification[key].push(...lastAddition[key]) + } + } + additions.length = 0 } // Add name attribute to the modifications to make it look better ; it won't be added otherwise as it would be the same if (!this.isEmpty(modifications[modifications.length - 1])) { diff --git a/test/unit/lib/mergeDeep.test.js b/test/unit/lib/mergeDeep.test.js index 35d6aeed..c114377e 100644 --- a/test/unit/lib/mergeDeep.test.js +++ b/test/unit/lib/mergeDeep.test.js @@ -2,7 +2,7 @@ const MergeDeep = require('../../../lib/mergeDeep') const YAML = require('js-yaml') const log = require('pino')('test.log') - + describe('MergeDeep Test', () => { it('CompareDeep extensive test', () => { const target = YAML.load(` @@ -116,38 +116,38 @@ branches: const expected = { additions: { repository: { - name: "test", - org: "decyjphr-org", - homepage: "https://newhome.github.io/", + name: 'test', + org: 'decyjphr-org', + homepage: 'https://newhome.github.io/', topics: [ - "red" + 'red' ], auto_init: true, has_issues: true, has_projects: true, has_downloads: true, allow_squash_merge: true, - default_branch: "develop" + default_branch: 'develop' }, labels: [ - { - name: "green", - color: "#B60205", - description: "An issue sswithss the system" - } + { + name: 'green', + color: '#B60205', + description: 'An issue sswithss the system' + } ], validator: { - pattern: "[a-zA-Z0-9_-]+" + pattern: '[a-zA-Z0-9_-]+' }, collaborators: [ - { - username: "regpaco", - permission: "pull" - } + { + username: 'regpaco', + permission: 'pull' + } ], branches: [ { - name: "feature1", + name: 'feature1', protection: { required_pull_request_reviews: { required_approving_review_count: 5, @@ -174,8 +174,8 @@ branches: }, modifications: { repository: { - description: "description of test repository", - name: "test" + description: 'description of test repository', + name: 'test' } }, hasChanges: true @@ -183,13 +183,13 @@ branches: const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) console.log(`source ${JSON.stringify(source, null, 2)}`) console.log(`target ${JSON.stringify(target, null, 2)}`) @@ -221,9 +221,9 @@ branches: contexts: [] enforce_admins: false `) - + const expected = { - additions: + additions: { protection: { required_pull_request_reviews: { @@ -237,26 +237,25 @@ branches: contexts: [] }, enforce_admins: false - } } - , - modifications: {}, - hasChanges: true - } - - const ignorableFields = [] - const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); - const mergeDeep = new MergeDeep( - log, - mockReturnGitHubContext, - ignorableFields - ); - const merged = mergeDeep.compareDeep(undefined, source) - console.log(`diffs ${JSON.stringify(merged, null, 2)}`) - expect(merged.additions).toEqual(expected.additions) - expect(merged.modifications.length).toEqual(expected.modifications.length) + }, + modifications: {}, + hasChanges: true + } + + const ignorableFields = [] + const mockReturnGitHubContext = jest.fn().mockReturnValue({ + request: () => {} + }) + const mergeDeep = new MergeDeep( + log, + mockReturnGitHubContext, + ignorableFields + ) + const merged = mergeDeep.compareDeep(undefined, source) + console.log(`diffs ${JSON.stringify(merged, null, 2)}`) + expect(merged.additions).toEqual(expected.additions) + expect(merged.modifications.length).toEqual(expected.modifications.length) }) it('CompareDeep Empty target Works', () => { @@ -272,45 +271,45 @@ branches: contexts: [] enforce_admins: false `) - + const expected = { additions: { - protection:{ - required_pull_request_reviews:{ - required_approving_review_count:2, - dismiss_stale_reviews:false, - require_code_owner_reviews:true, - dismissal_restrictions:{}}, - required_status_checks:{ - strict:true, - contexts:[] - }, - enforce_admins:false - } + protection: { + required_pull_request_reviews: { + required_approving_review_count: 2, + dismiss_stale_reviews: false, + require_code_owner_reviews: true, + dismissal_restrictions: {} + }, + required_status_checks: { + strict: true, + contexts: [] + }, + enforce_admins: false } - , - modifications:{}, - hasChanges: true - } - const ignorableFields = [] - const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); - const mergeDeep = new MergeDeep( - log, - mockReturnGitHubContext, - ignorableFields - ); - const merged = mergeDeep.compareDeep({}, source) - console.log(`diffs ${JSON.stringify(merged, null, 2)}`) - expect(merged.additions).toEqual(expected.additions) - expect(merged.modifications.length).toEqual(expected.modifications.length) - - const overrideConfig = mergeDeep.mergeDeep({}, {}, source) - const same = mergeDeep.compareDeep(overrideConfig, source) - expect(same.additions).toEqual({}) - expect(same.modifications).toEqual({}) + }, + modifications: {}, + hasChanges: true + } + const ignorableFields = [] + const mockReturnGitHubContext = jest.fn().mockReturnValue({ + request: () => {} + }) + const mergeDeep = new MergeDeep( + log, + mockReturnGitHubContext, + ignorableFields + ) + const merged = mergeDeep.compareDeep({}, source) + console.log(`diffs ${JSON.stringify(merged, null, 2)}`) + expect(merged.additions).toEqual(expected.additions) + expect(merged.modifications.length).toEqual(expected.modifications.length) + + const overrideConfig = mergeDeep.mergeDeep({}, {}, source) + const same = mergeDeep.compareDeep(overrideConfig, source) + expect(same.additions).toEqual({}) + expect(same.modifications).toEqual({}) }) it('CompareDeep Test when target is from the api', () => { @@ -378,7 +377,7 @@ branches: branches: [ { name: 'master', - protection: protection + protection } ] } @@ -408,7 +407,7 @@ branches: dismiss_stale_reviews: false } }, - name: "master" + name: 'master' } ] }, @@ -417,13 +416,13 @@ branches: const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) console.log(`source ${JSON.stringify(source, null, 2)}`) console.log(`target ${JSON.stringify(target, null, 2)}`) @@ -438,9 +437,9 @@ branches: }) it('Merge labels with ignorable extra info Works', () => { - const source = { entries: [{"id":3954990840,"node_id":"LA_kwDOHC6_Gc7rvF74","url":"https://api.github.com/repos/decyjphr-org/test2/labels/bug","name":"bug","color":"CC0000","default":true,"description":"An issue with the system"},{"id":4015763857,"node_id":"LA_kwDOHC6_Gc7vW7GR","url":"https://api.github.com/repos/decyjphr-org/test2/labels/feature","name":"feature","color":"336699","default":false,"description":"New functionality."},{"id":4015763934,"node_id":"LA_kwDOHC6_Gc7vW7He","url":"https://api.github.com/repos/decyjphr-org/test2/labels/first-timers-only","name":"first-timers-only","color":"326699","default":false,"description":null},{"id":4015763984,"node_id":"LA_kwDOHC6_Gc7vW7IQ","url":"https://api.github.com/repos/decyjphr-org/test2/labels/new-label","name":"new-label","color":"326699","default":false,"description":null}]} - const target = { entries: [{"name":"bug","color":"CC0000","description":"An issue with the system"},{"name":"feature","color":"336699","description":"New functionality."},{"name":"first-timers-only","oldname":"Help Wanted","color":"326699"},{"name":"new-label","oldname":"Help Wanted","color":"326699"}]} - + const source = { entries: [{ id: 3954990840, node_id: 'LA_kwDOHC6_Gc7rvF74', url: 'https://api.github.com/repos/decyjphr-org/test2/labels/bug', name: 'bug', color: 'CC0000', default: true, description: 'An issue with the system' }, { id: 4015763857, node_id: 'LA_kwDOHC6_Gc7vW7GR', url: 'https://api.github.com/repos/decyjphr-org/test2/labels/feature', name: 'feature', color: '336699', default: false, description: 'New functionality.' }, { id: 4015763934, node_id: 'LA_kwDOHC6_Gc7vW7He', url: 'https://api.github.com/repos/decyjphr-org/test2/labels/first-timers-only', name: 'first-timers-only', color: '326699', default: false, description: null }, { id: 4015763984, node_id: 'LA_kwDOHC6_Gc7vW7IQ', url: 'https://api.github.com/repos/decyjphr-org/test2/labels/new-label', name: 'new-label', color: '326699', default: false, description: null }] } + const target = { entries: [{ name: 'bug', color: 'CC0000', description: 'An issue with the system' }, { name: 'feature', color: '336699', description: 'New functionality.' }, { name: 'first-timers-only', oldname: 'Help Wanted', color: '326699' }, { name: 'new-label', oldname: 'Help Wanted', color: '326699' }] } + const expected = { additions: {}, modifications: {} @@ -448,28 +447,27 @@ branches: const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) // console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged.additions).toEqual(expected.additions) expect(merged.modifications.length).toEqual(expected.modifications.length) - const overrideConfig = mergeDeep.mergeDeep({}, target, source) // console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) const same = mergeDeep.compareDeep(overrideConfig, target) // console.log(`new diffs ${JSON.stringify(same, null, 2)}`) expect(same.additions).toEqual({}) expect(same.modifications).toEqual({}) - }) + }) - it('Merge labels ', () => { + it('Merge labels ', () => { const source = YAML.load(` repository: name: new @@ -487,68 +485,66 @@ branches: home: old home `) const expected = { - additions: { - labels: [ - { - name: "green", - color: "#B60205", - description: "An issue sswithss the system" - } - ] - }, - modifications: {}, - hasChanges: true + additions: { + labels: [ + { + name: 'green', + color: '#B60205', + description: 'An issue sswithss the system' + } + ] + }, + modifications: {}, + hasChanges: true } const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) // console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged.additions).toEqual(expected.additions) expect(merged.modifications.length).toEqual(expected.modifications.length) - const overrideConfig = mergeDeep.mergeDeep({}, target, source) // console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) const same = mergeDeep.compareDeep(overrideConfig, source) // console.log(`new diffs ${JSON.stringify(same, null, 2)}`) expect(same.additions).toEqual({}) expect(same.modifications).toEqual({}) - }) + }) it('Compare nested arrays deep', () => { - const source = {colors: [{name: "blue", color: "green"},{name: "newone",color: "red"},{ name: "uber",color: "yellow"}]} - const target = {colors: [{name: "blue", color: "blue", anotherArray:[1,2,3]}, {name: "blu", color: "blu"}]} - + const source = { colors: [{ name: 'blue', color: 'green' }, { name: 'newone', color: 'red' }, { name: 'uber', color: 'yellow' }] } + const target = { colors: [{ name: 'blue', color: 'blue', anotherArray: [1, 2, 3] }, { name: 'blu', color: 'blu' }] } + // Note: properties in the target and not in source won't show up as deletions. This is by design. const expected = { - additions:{colors: [{name:"newone",color:"red"},{name:"uber",color:"yellow"}]}, - modifications: {colors: [{color:"green",name:"blue"}]}, - hasChanges:true + additions: { colors: [{ name: 'newone', color: 'red' }, { name: 'uber', color: 'yellow' }] }, + modifications: { colors: [{ color: 'green', name: 'blue' }] }, + hasChanges: true } const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged.additions).toEqual(expected.additions) expect(merged.modifications.length).toEqual(expected.modifications.length) - const overrideConfig = mergeDeep.mergeDeep({}, target, source) console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) const same = mergeDeep.compareDeep(overrideConfig, source) @@ -558,26 +554,25 @@ branches: }) it('Merge array of topics', () => { - const source = { entries: ["blue","green","newone","red","uber","yellow"]} - const target = { entries: ["red","blu"]} - + const source = { entries: ['blue', 'green', 'newone', 'red', 'uber', 'yellow'] } + const target = { entries: ['red', 'blu'] } const expected = { additions: { - entries:["blue","green","newone","uber","yellow"] + entries: ['blue', 'green', 'newone', 'uber', 'yellow'] }, - modifications:{} + modifications: {} } const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged.additions).toEqual(expected.additions) @@ -609,7 +604,7 @@ branches: block_creations: true required_conversation_resolution: true `) - + const target = YAML.load(` branches: - name: default @@ -619,53 +614,53 @@ branches: contexts: - "Lint, compile and build" `) - + const expected = { - additions:{}, + additions: {}, modifications: { branches: [{ + name: 'default', protection: { required_pull_request_reviews: { - required_approving_review_count:1, - dismiss_stale_reviews:true, - require_code_owner_reviews:true + required_approving_review_count: 1, + dismiss_stale_reviews: true, + require_code_owner_reviews: true }, - required_status_checks:{ - contexts:[ - "🔃 pre-commit" + required_status_checks: { + contexts: [ + '🔃 pre-commit' ] }, - enforce_admins:true, - block_creations:true, - required_conversation_resolution:true + enforce_admins: true, + block_creations: true, + required_conversation_resolution: true } }] }, - hasChanges:true + hasChanges: true } const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) // console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged.additions).toEqual(expected.additions) expect(merged.modifications.length).toEqual(expected.modifications.length) - - + expect(merged.modifications).toEqual(expected.modifications) const overrideConfig = mergeDeep.mergeDeep({}, target, source) // console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) const same = mergeDeep.compareDeep(overrideConfig, target) // console.log(`new diffs ${JSON.stringify(same, null, 2)}`) expect(same.additions).toEqual({}) expect(same.modifications).toEqual({}) - }) + }) it('Simple compare', () => { const source = YAML.load(` @@ -677,30 +672,29 @@ branches: values: a: [b] `) - + const target = YAML.load(` x: - name: default values: a: [c,a] `) - - const expected = JSON.parse(`{"additions":{"x":[{"name":"new","values":{"a":["b"]}}]},"modifications":{"x":[{"values":{"a":["b"]},"name":"default"}]},"hasChanges":true}`) + + const expected = JSON.parse('{"additions":{"x":[{"name":"new","values":{"a":["b"]}}]},"modifications":{"x":[{"values":{"a":["b"]},"name":"default"}]},"hasChanges":true}') const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) // console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged.additions).toEqual(expected.additions) expect(merged.modifications.length).toEqual(expected.modifications.length) - const overrideConfig = mergeDeep.mergeDeep({}, target, source) // console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) const same = mergeDeep.compareDeep(overrideConfig, target) @@ -708,9 +702,8 @@ branches: expect(same.additions).toEqual({}) expect(same.modifications).toEqual({}) }) - - it('Repo test', () => { + it('Repo test', () => { const source = YAML.load(` repository: name: test @@ -741,28 +734,28 @@ branches: - uber - newone `) - + const expected = { additions: { repository: { - name: "test", - org: "decyjphr-org", - homepage: "https://newhome.github.io/", + name: 'test', + org: 'decyjphr-org', + homepage: 'https://newhome.github.io/', topics: [ - "red" + 'red' ], auto_init: true, has_issues: true, has_projects: true, has_downloads: true, allow_squash_merge: true, - default_branch: "develop" + default_branch: 'develop' } }, modifications: { repository: { - description: "description of test repository", - name: "test" + description: 'description of test repository', + name: 'test' } }, hasChanges: true @@ -770,30 +763,29 @@ branches: const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) - //console.log(`diffs ${JSON.stringify(merged, null, 2)}`) + // console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged.additions).toEqual(expected.additions) expect(merged.modifications.length).toEqual(expected.modifications.length) - //console.log(`target = ${JSON.stringify(target, null, 2)}`) + // console.log(`target = ${JSON.stringify(target, null, 2)}`) const overrideConfig = mergeDeep.mergeDeep({}, target, source) - //console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) + // console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) const same = mergeDeep.compareDeep(overrideConfig, source) - //console.log(`new diffs ${JSON.stringify(same, null, 2)}`) + // console.log(`new diffs ${JSON.stringify(same, null, 2)}`) expect(same.additions).toEqual({}) expect(same.modifications).toEqual({}) - }) - + it('Autolinks test', () => { const source = YAML.load(` entries: @@ -805,14 +797,14 @@ entries: const target = [] const expected = { additions: { - entries: [ + entries: [ { - key_prefix: "ASDF-", - url_template: "https://jira.company.com/browse/ASDF-" + key_prefix: 'ASDF-', + url_template: 'https://jira.company.com/browse/ASDF-' }, { - key_prefix: "BOLIGRAFO-", - url_template: "https://jira.company.com/browse/BOLIGRAFO-" + key_prefix: 'BOLIGRAFO-', + url_template: 'https://jira.company.com/browse/BOLIGRAFO-' } ] }, @@ -821,8 +813,8 @@ entries: } const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, @@ -850,32 +842,32 @@ entries: `) const expected = { additions: { - entries: [ + entries: [ { - key_prefix: "BOLIGRAFO-", - url_template: "https://jira.company.com/browse/BOLIGRAFO-" + key_prefix: 'BOLIGRAFO-', + url_template: 'https://jira.company.com/browse/BOLIGRAFO-' } ] }, modifications: { }, deletions: { - entries: [ { - key_prefix: "BOLSIGRAFO-", - url_template: "https://jira.company.com/browse/BOLIGRAFO-" - },] + entries: [{ + key_prefix: 'BOLSIGRAFO-', + url_template: 'https://jira.company.com/browse/BOLIGRAFO-' + }] }, hasChanges: true } const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged).toEqual(expected) @@ -884,13 +876,13 @@ entries: it('CompareDeep does not mutate source object', () => { const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const target = { teams: [ { name: 'developers' }, @@ -901,25 +893,25 @@ entries: teams: ['developers'] } mergeDeep.compareDeep(target, source) - + // const result = mergeDeep.compareDeep(target, source) // console.log(`source ${JSON.stringify(source, null, 2)}`) // console.log(`target ${JSON.stringify(target, null, 2)}`) // console.log(`result ${JSON.stringify(result, null, 2)}`) - + expect(source.teams).toEqual(['developers']) }) it('CompareDeep produces correct result for arrays of named objects', () => { const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const target = { teams: [ { name: 'developers' }, @@ -930,25 +922,24 @@ entries: teams: ['developers'] } const result = mergeDeep.compareDeep(target, source) - + console.log(`source ${JSON.stringify(source, null, 2)}`) console.log(`target ${JSON.stringify(target, null, 2)}`) console.log(`result ${JSON.stringify(result, null, 2)}`) - + expect(result.deletions.teams).toEqual([{ name: 'marketing' }]) }) - - + it('CompareDeep result has changes when source is empty and target is not', () => { const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const target = { required_pull_request_reviews: { dismissal_restrictions: { @@ -958,7 +949,7 @@ entries: } } } - + const source = { required_pull_request_reviews: { dismissal_restrictions: { @@ -969,20 +960,20 @@ entries: } } const result = mergeDeep.compareDeep(target, source) - + expect(result.hasChanges).toBeTruthy() }) - + it('CompareDeep result has no change when source and target match', () => { const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const target = { required_pull_request_reviews: { dismissal_restrictions: { @@ -1003,89 +994,89 @@ entries: } } const result = mergeDeep.compareDeep(target, source) - + expect(result.hasChanges).toBeFalsy() }) - + it('CompareDeep finds modifications on top-level arrays with different ordering', () => { const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const target = [ - { username: 'collaborator-1' }, - { username: 'collaborator-2' }, - ] + { username: 'collaborator-1' }, + { username: 'collaborator-2' } + ] const source = [ - { username: 'collaborator-2' }, - { username: 'collaborator-1' }, - ] + { username: 'collaborator-2' }, + { username: 'collaborator-1' } + ] const result = mergeDeep.compareDeep(target, source) - + expect(result.hasChanges).toBeFalsy() }) - + it('CompareDeep does not report changes for matching empty targets', () => { const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const target = [] const source = [] const result = mergeDeep.compareDeep(target, source) - + expect(result.hasChanges).toBeFalsy() }) it('Compare array as toplevel object', () => { const source = [ { - "username": "iksafagr", - "pendinginvite": false, - "permission": "admin" + username: 'iksafagr', + pendinginvite: false, + permission: 'admin' } ] const target = [ { - "username": "iksafagr", - "pendinginvite": false, - "permission": "admin" + username: 'iksafagr', + pendinginvite: false, + permission: 'admin' }, { - "username": "iksathrr", - "pendinginvite": false, - "permission": "admin" + username: 'iksathrr', + pendinginvite: false, + permission: 'admin' } ] - + const expected = { deletions: [{ - "username": "iksathrr", - "pendinginvite": false, - "permission": "admin" - }], - modifications:{} + username: 'iksathrr', + pendinginvite: false, + permission: 'admin' + }], + modifications: {} } const ignorableFields = [] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) console.log(`diffs ${JSON.stringify(merged, null, 2)}`) expect(merged.deletions).toEqual(expected.deletions) @@ -1097,230 +1088,541 @@ entries: expect(same.modifications).toEqual({}) expect(same.modifications).toEqual({}) }) - + it('Ruleset Compare Works when no changes', () => { const target = [ { - "id": 68617, - "name": "demo", - "target": "branch", - "source_type": "Organization", - "source": "decyjphr-scale-test", - "enforcement": "disabled", - "conditions": { - "ref_name": { - "exclude": [ - "refs/heads/oldmaster" + id: 68617, + name: 'demo', + target: 'branch', + source_type: 'Organization', + source: 'decyjphr-scale-test', + enforcement: 'disabled', + conditions: { + ref_name: { + exclude: [ + 'refs/heads/oldmaster' ], - "include": [ - "~DEFAULT_BRANCH" + include: [ + '~DEFAULT_BRANCH' ] }, - "repository_name": { - "exclude": [ - "test", - "test1" + repository_name: { + exclude: [ + 'test', + 'test1' ], - "include": [ - "test*" + include: [ + 'test*' ], - "protected": true + protected: true } }, - "rules": [ + rules: [ { - "type": "creation" + type: 'creation' }, { - "type": "update" + type: 'update' }, { - "type": "deletion" + type: 'deletion' }, { - "type": "required_linear_history" + type: 'required_linear_history' }, { - "type": "required_signatures" + type: 'required_signatures' }, { - "type": "pull_request", - "parameters": { - "require_code_owner_review": true, - "require_last_push_approval": true, - "dismiss_stale_reviews_on_push": true, - "required_approving_review_count": 10, - "required_review_thread_resolution": true + type: 'pull_request', + parameters: { + require_code_owner_review: true, + require_last_push_approval: true, + dismiss_stale_reviews_on_push: true, + required_approving_review_count: 10, + required_review_thread_resolution: true } }, { - "type": "commit_message_pattern", - "parameters": { - "name": "test commit_message_pattern", - "negate": true, - "pattern": "skip*", - "operator": "starts_with" + type: 'commit_message_pattern', + parameters: { + name: 'test commit_message_pattern', + negate: true, + pattern: 'skip*', + operator: 'starts_with' } }, { - "type": "commit_author_email_pattern", - "parameters": { - "name": "test commit_author_email_pattern", - "negate": false, - "pattern": "^.*@example.com$", - "operator": "regex" + type: 'commit_author_email_pattern', + parameters: { + name: 'test commit_author_email_pattern', + negate: false, + pattern: '^.*@example.com$', + operator: 'regex' } }, { - "type": "committer_email_pattern", - "parameters": { - "name": "test committer_email_pattern", - "negate": false, - "pattern": "^.*@example.com$", - "operator": "regex" + type: 'committer_email_pattern', + parameters: { + name: 'test committer_email_pattern', + negate: false, + pattern: '^.*@example.com$', + operator: 'regex' } }, { - "type": "branch_name_pattern", - "parameters": { - "name": "test branch_name_pattern", - "negate": false, - "pattern": ".*/.*", - "operator": "regex" + type: 'branch_name_pattern', + parameters: { + name: 'test branch_name_pattern', + negate: false, + pattern: '.*/.*', + operator: 'regex' } } ], - "node_id": "RRS_lACkVXNlcs4GH_FizgABDAk", - "_links": { - "self": { - "href": "https://api.github.com/orgs/decyjphr-scale-test/rulesets/68617" + node_id: 'RRS_lACkVXNlcs4GH_FizgABDAk', + _links: { + self: { + href: 'https://api.github.com/orgs/decyjphr-scale-test/rulesets/68617' }, - "html": { - "href": "https://github.com/organizations/decyjphr-scale-test/settings/rules/68617" + html: { + href: 'https://github.com/organizations/decyjphr-scale-test/settings/rules/68617' } }, - "created_at": "2023-08-11T15:55:49.071Z", - "updated_at": "2023-08-11T15:55:49.139Z", - "bypass_actors": [ + created_at: '2023-08-11T15:55:49.071Z', + updated_at: '2023-08-11T15:55:49.139Z', + bypass_actors: [ { - "actor_id": 1, - "actor_type": "OrganizationAdmin", - "bypass_mode": "pull_request" + actor_id: 1, + actor_type: 'OrganizationAdmin', + bypass_mode: 'pull_request' } ] } - ] + ] const source = [ { - "name": "demo", - "target": "branch", - "enforcement": "disabled", - "bypass_actors": [ + name: 'demo', + target: 'branch', + enforcement: 'disabled', + bypass_actors: [ { - "actor_id": 1, - "actor_type": "OrganizationAdmin", - "bypass_mode": "pull_request" + actor_id: 1, + actor_type: 'OrganizationAdmin', + bypass_mode: 'pull_request' } ], - "conditions": { - "ref_name": { - "include": [ - "~DEFAULT_BRANCH" + conditions: { + ref_name: { + include: [ + '~DEFAULT_BRANCH' ], - "exclude": [ - "refs/heads/oldmaster" + exclude: [ + 'refs/heads/oldmaster' ] }, - "repository_name": { - "include": [ - "test*" + repository_name: { + include: [ + 'test*' ], - "exclude": [ - "test", - "test1" + exclude: [ + 'test', + 'test1' ], - "protected": true + protected: true } }, - "rules": [ + rules: [ { - "type": "creation" + type: 'creation' }, { - "type": "update" + type: 'update' }, { - "type": "deletion" + type: 'deletion' }, { - "type": "required_linear_history" + type: 'required_linear_history' }, { - "type": "required_signatures" + type: 'required_signatures' }, { - "type": "pull_request", - "parameters": { - "dismiss_stale_reviews_on_push": true, - "require_code_owner_review": true, - "require_last_push_approval": true, - "required_approving_review_count": 10, - "required_review_thread_resolution": true + type: 'pull_request', + parameters: { + dismiss_stale_reviews_on_push: true, + require_code_owner_review: true, + require_last_push_approval: true, + required_approving_review_count: 10, + required_review_thread_resolution: true } }, { - "type": "commit_message_pattern", - "parameters": { - "name": "test commit_message_pattern", - "negate": true, - "operator": "starts_with", - "pattern": "skip*" + type: 'commit_message_pattern', + parameters: { + name: 'test commit_message_pattern', + negate: true, + operator: 'starts_with', + pattern: 'skip*' } }, { - "type": "commit_author_email_pattern", - "parameters": { - "name": "test commit_author_email_pattern", - "negate": false, - "operator": "regex", - "pattern": "^.*@example.com$" + type: 'commit_author_email_pattern', + parameters: { + name: 'test commit_author_email_pattern', + negate: false, + operator: 'regex', + pattern: '^.*@example.com$' } }, { - "type": "committer_email_pattern", - "parameters": { - "name": "test committer_email_pattern", - "negate": false, - "operator": "regex", - "pattern": "^.*@example.com$" + type: 'committer_email_pattern', + parameters: { + name: 'test committer_email_pattern', + negate: false, + operator: 'regex', + pattern: '^.*@example.com$' } }, { - "type": "branch_name_pattern", - "parameters": { - "name": "test branch_name_pattern", - "negate": false, - "operator": "regex", - "pattern": ".*/.*" + type: 'branch_name_pattern', + parameters: { + name: 'test branch_name_pattern', + negate: false, + operator: 'regex', + pattern: '.*/.*' } } ] } - ] + ] const ignorableFields = ['id', 'node_id', 'default', 'url'] const mockReturnGitHubContext = jest.fn().mockReturnValue({ - request: () => {}, - }); + request: () => {} + }) const mergeDeep = new MergeDeep( log, mockReturnGitHubContext, ignorableFields - ); + ) const merged = mergeDeep.compareDeep(target, source) expect(merged.hasChanges).toBeFalsy() - //console.log(`source ${JSON.stringify(source, null, 2)}`) - //console.log(`target ${JSON.stringify(target, null, 2)}`) - //console.log(`diffs ${JSON.stringify(merged, null, 2)}`) + // console.log(`source ${JSON.stringify(source, null, 2)}`) + // console.log(`target ${JSON.stringify(target, null, 2)}`) + // console.log(`diffs ${JSON.stringify(merged, null, 2)}`) + }) + + it('Merge array as the second level with same id - add new value', () => { + const target = YAML.load(` + environments: + - name: production + variables: + - name: ROUTE53_HOSTNAME + value: 'https://foo-prod.com' + ` + ) + + const source = YAML.load(` + environments: + - name: production + variables: + - name: ROUTE53_HOSTNAME + value: 'https://foo-prod.com' + - name: SECOND_VARIABLE + value: 'My_SECOND_VARIABLE' + `) + + const expected = { + additions: {}, + modifications: { + environments: [{ + name: 'production', + variables: [ + { + name: 'SECOND_VARIABLE', + value: 'My_SECOND_VARIABLE' + } + ] + }] + }, + deletions: {}, + hasChanges: true + } + + const ignorableFields = [] + const mockReturnGitHubContext = jest.fn().mockReturnValue({ + request: () => {} + }) + const mergeDeep = new MergeDeep( + log, + mockReturnGitHubContext, + ignorableFields + ) + const merged = mergeDeep.compareDeep(target, source) + console.log(`source ${JSON.stringify(source, null, 2)}`) + console.log(`target ${JSON.stringify(target, null, 2)}`) + console.log(`diffs ${JSON.stringify(merged, null, 2)}`) + expect(merged.additions).toEqual(expected.additions) + expect(merged.deletions).toEqual(expected.deletions) + expect(merged.modifications).toEqual(expected.modifications) + expect(merged.modifications.length).toEqual(expected.modifications.length) + + console.log(`target = ${JSON.stringify(target, null, 2)}`) + const overrideConfig = mergeDeep.mergeDeep({}, target, source) + + console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) + + const same = mergeDeep.compareDeep(overrideConfig, target) + console.log(`new diffs ${JSON.stringify(same, null, 2)}`) + expect(same.additions).toEqual({}) + expect(same.modifications).toEqual({}) + }) + + it('Identify modification and addition to the environment variables correctly', () => { + const target = YAML.load(` + environments: + - name: production + variables: + - name: ROUTE53_HOSTNAME + value: 'https://foo-prod.com' + ` + ) + + const source = YAML.load(` + environments: + - name: production + variables: + - name: ROUTE53_HOSTNAME + value: 'https://foo-prod.com.changed.value' + - name: SECOND_VARIABLE + value: 'My_SECOND_VARIABLE' + - name: development + variables: + - name: ROUTE53_HOSTNAME_DEVL + value: 'https://foo-devl.com' + - name: SECOND_VARIABLE_DEVL + value: 'My_SECOND_VARIABLE_DEVL' + `) + + const expected = { + additions: { + environments: [{ + name: 'development', + variables: [ + { + name: 'ROUTE53_HOSTNAME_DEVL', + value: 'https://foo-devl.com' + }, + { + name: 'SECOND_VARIABLE_DEVL', + value: 'My_SECOND_VARIABLE_DEVL' + } + ] + }] + }, + modifications: { + environments: [{ + name: 'production', + variables: [ + { + name: 'ROUTE53_HOSTNAME', + value: 'https://foo-prod.com.changed.value' + }, + { + name: 'SECOND_VARIABLE', + value: 'My_SECOND_VARIABLE' + } + ] + }] + }, + deletions: {}, + hasChanges: true + } + + const ignorableFields = [] + const mockReturnGitHubContext = jest.fn().mockReturnValue({ + request: () => {} + }) + const mergeDeep = new MergeDeep( + log, + mockReturnGitHubContext, + ignorableFields + ) + const merged = mergeDeep.compareDeep(target, source) + console.log(`source ${JSON.stringify(source, null, 2)}`) + console.log(`target ${JSON.stringify(target, null, 2)}`) + console.log(`diffs ${JSON.stringify(merged, null, 2)}`) + expect(merged.additions).toEqual(expected.additions) + expect(merged.deletions).toEqual(expected.deletions) + expect(merged.modifications).toEqual(expected.modifications) + expect(merged.modifications.length).toEqual(expected.modifications.length) + + console.log(`target = ${JSON.stringify(target, null, 2)}`) + const overrideConfig = mergeDeep.mergeDeep({}, target, source) + + console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) + + const same = mergeDeep.compareDeep(overrideConfig, target) + console.log(`new diffs ${JSON.stringify(same, null, 2)}`) + expect(same.additions).toEqual({}) + expect(same.modifications).toEqual({}) + }) + + it('CompareDeep branches', () => { + const target = YAML.load(` +branches: + # If the name of the branch is default, it will create a branch protection for the default branch in the repo + - name: default + # https://developer.github.com/v3/repos/branches/#update-branch-protection + # Branch Protection settings. Set to null to disable + protection: + # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. + required_pull_request_reviews: + # The number of approvals required. (1-6) + required_approving_review_count: 2 + # Dismiss approved reviews automatically when a new commit is pushed. + dismiss_stale_reviews: true + # Blocks merge until code owners have reviewed. + require_code_owner_reviews: true + # Specify which users and teams can dismiss pull request reviews. Pass an empty dismissal_restrictions object to disable. User and team dismissal_restrictions are only available for organization-owned repositories. Omit this parameter for personal repositories. + dismissal_restrictions: + users: [] + teams: [] + # Required. Require status checks to pass before merging. Set to null to disable + required_status_checks: + # Required. Require branches to be up to date before merging. + strict: true + # Required. The list of status checks to require in order to merge into this branch + contexts: [] + # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. + enforce_admins: true + # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable. + restrictions: + apps: [] + users: [] + teams: []`) + + const source = YAML.load(` + branches: + - name: feature1 + # https://developer.github.com/v3/repos/branches/#update-branch-protection + # Branch Protection settings. Set to null to disable + protection: + # Required. Require at least one approving review on a pull request, before merging. Set to null to disable. + required_pull_request_reviews: + # The number of approvals required. (1-6) + required_approving_review_count: 5 + # Dismiss approved reviews automatically when a new commit is pushed. + dismiss_stale_reviews: true + # Blocks merge until code owners have reviewed. + require_code_owner_reviews: true + # Specify which users and teams can dismiss pull request reviews. Pass an empty dismissal_restrictions object to disable. User and team dismissal_restrictions are only available for organization-owned repositories. Omit this parameter for personal repositories. + dismissal_restrictions: + users: [] + teams: [] + # Required. Require status checks to pass before merging. Set to null to disable + required_status_checks: + # Required. Require branches to be up to date before merging. + strict: true + # Required. The list of status checks to require in order to merge into this branch + contexts: [] + # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. + enforce_admins: false + # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable. + restrictions: + apps: [] + users: [] + teams: []`) + + const expected = { + additions: { + branches: [ + { + name: 'feature1', + protection: { + required_pull_request_reviews: { + required_approving_review_count: 5, + dismiss_stale_reviews: true, + require_code_owner_reviews: true, + dismissal_restrictions: { + users: [], + teams: [] + } + }, + required_status_checks: { + strict: true, + contexts: [] + }, + enforce_admins: false, + restrictions: { + apps: [], + users: [], + teams: [] + } + } + } + ] + }, + modifications: {}, + deletions: { + branches: [ + { + name: 'default', + protection: { + required_pull_request_reviews: { + required_approving_review_count: 2, + dismiss_stale_reviews: true, + require_code_owner_reviews: true, + dismissal_restrictions: { + teams: [], + users: [] + } + }, + required_status_checks: { + strict: true, + contexts: [] + }, + enforce_admins: true, + restrictions: { + apps: [], + teams: [], + users: [] + } + } + } + ] + }, + hasChanges: true + } + + const ignorableFields = [] + const mockReturnGitHubContext = jest.fn().mockReturnValue({ + request: () => {} + }) + const mergeDeep = new MergeDeep( + log, + mockReturnGitHubContext, + ignorableFields + ) + const merged = mergeDeep.compareDeep(target, source) + console.log(`source ${JSON.stringify(source, null, 2)}`) + console.log(`target ${JSON.stringify(target, null, 2)}`) + console.log(`diffs ${JSON.stringify(merged, null, 2)}`) + expect(merged.additions).toEqual(expected.additions) + expect(merged.deletions).toEqual(expected.deletions) + expect(merged.modifications).toEqual(expected.modifications) + expect(merged.modifications.length).toEqual(expected.modifications.length) + // // + console.log(`target = ${JSON.stringify(target, null, 2)}`) + const overrideConfig = mergeDeep.mergeDeep({}, target, source) + + console.log(`overrideConfig = ${JSON.stringify(overrideConfig, null, 2)}`) + + const same = mergeDeep.compareDeep(overrideConfig, target) + console.log(`new diffs ${JSON.stringify(same, null, 2)}`) + expect(same.additions).toEqual({}) + expect(same.modifications).toEqual({}) }) - })