UNPKG

branch-remover

Version:

A small application for quickly removing unnecessary branches from GitHub.

884 lines (726 loc) 24.6 kB
import { expect } from 'chai'; import fs from 'fs'; import streams from 'memory-streams'; import readline from 'readline'; import sinon from 'sinon'; import { Writable } from 'stream'; import winston from 'winston'; import { BranchRemoverOptionsBuilder } from '../src/BranchRemoverOptionsBuilder'; import { Branch, BranchRemoveCancelationReason, BranchRemoverContext, BranchRemoverOptionsIgnoreFunction, BranchRemoverOptionsRemoveFunction, Logger, } from '../src/Core'; import { branchInfoFormatter } from '../src/Formatters'; describe('BranchRemoverOptionsBuilder', () => { let context: BranchRemoverContext = null; let writer: Writable = null; const parseLog = <T = any>(): Array<T> => { const jsonString = '[' + writer.toString().trim().replace(/[\r\n]+/g, ',') + ']'; return JSON.parse(jsonString); }; afterEach((): void => { writer = new streams.WritableStream(); const logger = new Logger({ levels: Logger.defaultLevels, level: 'debug', transports: [ new winston.transports.Stream({ stream: writer, }), ], }); context = { logger, }; }); describe('ignore', (): void => { it('the ignore property should be a function', (): void => { const builder = new BranchRemoverOptionsBuilder(); builder.ignore('test'); const options = builder.build(); expect(typeof options.ignore) .to.be.equal('function'); }); it('the ignore function should return TRUE', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.ignore('^(master|prod|release)$'); const options = builder.build(); const ignore = options.ignore as BranchRemoverOptionsIgnoreFunction; expect( await ignore({ context, branchName: 'master', }) ).to.be.true; expect( await ignore({ context, branchName: 'release', }) ).to.be.true; }); it('the ignore function should return FALSE', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.ignore('^(master|prod|release)$'); const options = builder.build(); const ignore = options.ignore as BranchRemoverOptionsIgnoreFunction; expect( await ignore({ context, branchName: 'issue-123', }) ).to.be.false; expect( await ignore({ context, branchName: 'master-prod', }) ).to.be.false; expect( await ignore({ context, branchName: 'prod555', }) ).to.be.false; expect( await ignore({ context, branchName: 'master-2', }) ).to.be.false; }); it('should return FALSE when the ignore property is not specified', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); const ignore = options.ignore as BranchRemoverOptionsIgnoreFunction; expect( await ignore({ context, branchName: 'issue-123', }) ).to.be.false; }); }); describe('remove', (): void => { it('the remove property should be a function', (): void => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); expect(typeof options.remove) .to.be.equal('function'); }); it('should return TRUE for the merged branch', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.quiet(); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; expect( await remove({ context, branch: { merged: true, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(), hasUncommittedChanges: false, }, }) ).to.be.true; }); it('should return FALSE for the unmerged branch', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; expect( await remove({ context, branch: { merged: false, name: 'issue-100', updatedDate: new Date(), hasUncommittedChanges: true, }, }) ).to.be.equal(BranchRemoveCancelationReason.UpdateDateOutOfRange); }); describe('merged', (): void => { it('should return TRUE for the merged branch when merge date less specified', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.quiet(); builder.merged(new Date(2021, 7, 8, 12, 0, 0)); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; expect( await remove({ context, branch: { merged: true, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(2021, 7, 8, 11, 59, 59), hasUncommittedChanges: true, }, }) ).to.be.true; }); it('should return TRUE for the merged branch when merge date equal to the specified', async(): Promise<void> => { const mergedDate = new Date(2021, 7, 8, 12, 0, 0); const builder = new BranchRemoverOptionsBuilder(); builder.quiet(); builder.merged(mergedDate); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; expect( await remove({ context, branch: { merged: true, name: 'issue-100', updatedDate: new Date(), mergedDate: mergedDate, hasUncommittedChanges: true, }, }) ).to.be.true; }); it('should return MergeDateOutOfRange for the merged branch when merge date greater than specified', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.quiet(); builder.merged(new Date(2021, 7, 8, 12, 0, 0)); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; expect( await remove({ context, branch: { merged: true, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(2021, 7, 8, 12, 0, 1), hasUncommittedChanges: true, }, }) ).to.be.equal(BranchRemoveCancelationReason.MergeDateOutOfRange); }); }); describe('stale', (): void => { it('should return TRUE for the unmerged branch when update date less specified', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.quiet(); builder.stale(new Date(2021, 7, 8, 12, 0, 0)); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; expect( await remove({ context, branch: { merged: false, name: 'issue-100', updatedDate: new Date(2021, 7, 8, 11, 59, 59), hasUncommittedChanges: true, }, }) ).to.be.true; }); it('should return TRUE for the unmerged branch when update date equal to the specified', async(): Promise<void> => { const updatedDate = new Date(2021, 7, 8, 12, 0, 0); const builder = new BranchRemoverOptionsBuilder(); builder.quiet(); builder.stale(updatedDate); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; expect( await remove({ context, branch: { merged: false, hasUncommittedChanges: true, name: 'issue-100', updatedDate, }, }) ).to.be.true; }); it('should return UpdateDateOutOfRange for the unmerged branch when update date greater than specified', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.quiet(); builder.stale(new Date(2021, 7, 8, 12, 0, 0)); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; expect( await remove({ context, branch: { merged: false, name: 'issue-100', updatedDate: new Date(2021, 7, 8, 12, 0, 1), hasUncommittedChanges: true, }, }) ).to.be.equal(BranchRemoveCancelationReason.UpdateDateOutOfRange); }); }); describe('quiet', (): void => { afterEach((): void => { sinon.restore(); }); it('should show a confirmation and be TRUE when the answer is "yes"', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; const readlineStub = { question: sinon.stub().callsFake( (query, callback) => { callback('yes'); } ), close: sinon.stub(), }; sinon.stub( readline, 'createInterface' ).returns(<any>readlineStub); const result = await remove({ context, branch: { merged: true, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(), hasUncommittedChanges: false, }, }); sinon.assert.calledWith( readlineStub.question, 'Do you want to remove merged branch "issue-100"? Merged 0 seconds ago [y/N] ', sinon.match.func ); sinon.assert.calledOnce(readlineStub.close); expect(result).to.be.true; }); it('should show a confirmation and be CanceledByUser when the answer is "no"', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; const branch: Branch = { merged: true, name: 'issue-100', // null - to avoid problems with different representation of dates updatedDate: null, mergedDate: null, hasUncommittedChanges: false, }; const readlineStub = { question: sinon.stub().callsFake( (query, callback) => { callback('no'); } ), close: sinon.stub(), }; sinon.stub( readline, 'createInterface' ).returns(<any>readlineStub); const result = await remove({ context, branch, }); sinon.assert.calledWith( readlineStub.question, 'Do you want to remove merged branch "issue-100"? Merged 0 seconds ago [y/N] ', sinon.match.func ); sinon.assert.calledOnce(readlineStub.close); expect(parseLog()) .to.be.deep.equal([ { branch, level: 'debug', message:'Can remove merged "{branch.name}", because no limit on merge date.', }, { branch, level: 'debug', message: 'Removing branch "{branch.name}" canceled by user.', } ]); expect(result).to.be.equal(BranchRemoveCancelationReason.CanceledByUser); }); }); describe('yes', (): void => { afterEach((): void => { sinon.restore(); }); it('should show a confirmation and be TRUE when the answer is empty', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.yes(); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; const readlineStub = { question: sinon.stub().callsFake( (query, callback) => { callback(''); } ), close: sinon.stub(), }; sinon.stub( readline, 'createInterface' ).returns(<any>readlineStub); const result = await remove({ context, branch: { merged: true, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(), hasUncommittedChanges: false, }, }); sinon.assert.calledWith( readlineStub.question, 'Do you want to remove merged branch "issue-100"? Merged 0 seconds ago [Y/n] ', sinon.match.func ); sinon.assert.calledOnce(readlineStub.close); expect(result).to.be.true; }); it('should show a confirmation and be CanceledByUser when the answer is empty', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; const branch: Branch = { merged: true, name: 'issue-100', // null - to avoid problems with different representation of dates updatedDate: null, mergedDate: null, hasUncommittedChanges: false, }; const readlineStub = { question: sinon.stub().callsFake( (query, callback) => { callback('no'); } ), close: sinon.stub(), }; sinon.stub( readline, 'createInterface' ).returns(<any>readlineStub); const result = await remove({ context, branch, }); sinon.assert.calledWith( readlineStub.question, 'Do you want to remove merged branch "issue-100"? Merged 0 seconds ago [y/N] ', sinon.match.func ); sinon.assert.calledOnce(readlineStub.close); expect(parseLog()) .to.be.deep.equal([ { branch, level: 'debug', message:'Can remove merged "{branch.name}", because no limit on merge date.', }, { branch, level: 'debug', message: 'Removing branch "{branch.name}" canceled by user.', } ]); expect(result).to.be.equal(BranchRemoveCancelationReason.CanceledByUser); }); }); describe('details', (): void => { afterEach((): void => { sinon.restore(); }); it('should display details for unmerged branch', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); builder.details(); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; sinon.stub(console, 'log'); const branch = { merged: false, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(), hasUncommittedChanges: false, }; await remove({ context, branch, }); sinon.assert.calledWith( <any>console.log, branchInfoFormatter(branch) ); }); it('should not display details for unmerged branch', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; sinon.stub(console, 'log'); const branch = { merged: false, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(), hasUncommittedChanges: false, }; await remove({ context, branch, }); sinon.assert.neverCalledWith( <any>console.log, branchInfoFormatter(branch) ); }); }); describe('beforeRemove', (): void => { const branch = { merged: false, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(), hasUncommittedChanges: false, }; it('should be true with default implementation', async(): Promise<void> => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); const result = await options.beforeRemove({ branch, context }); expect(result).to.be.true; }); it('should output command with branch name, and the result should be true', async(): Promise<void> => { const command = 'echo {branch}'; const commandToExec = `echo ${branch.name}`; const builder = new BranchRemoverOptionsBuilder(); const options = builder .beforeRemove(command) .build(); const result = await options.beforeRemove({ branch, context }); const [ commandPreparation, commandResult, ] = parseLog(); expect(commandPreparation.source).to.be.equal(command); expect(commandPreparation.parsed).to.be.equal(commandToExec); expect(commandResult.command).to.be.equal(commandToExec); expect(commandResult.stdout).to.be.equal(branch.name); expect(commandResult.stderr).to.be.empty; expect(result).to.be.true; }); it('should escape special characters', async(): Promise<void> => { const command = 'echo "\\{branch} {branch} \\{branch\\} {branch\\}"'; const commandToExec = `echo "{branch} ${branch.name} {branch} {branch}"`; const builder = new BranchRemoverOptionsBuilder(); const options = builder .beforeRemove(command) .build(); await options.beforeRemove({ branch, context }); const [ commandPreparation, commandResult, ] = parseLog(); expect(commandPreparation.source).to.be.equal(command); expect(commandPreparation.parsed).to.be.equal(commandToExec); expect(commandResult.command).to.be.equal(commandToExec); expect(commandResult.stdout).to.be.equal(`{branch} ${branch.name} {branch} {branch}`); expect(commandResult.stderr).to.be.empty; }); it('should be thrown an exception', (): void => { const command = 'exit 1'; const builder = new BranchRemoverOptionsBuilder(); const options = builder .beforeRemove(command) .build(); expect( async(): Promise<void> => { await options.beforeRemove({ branch, context }); } ).to.be.throw; }); }); describe('afterRemove', (): void => { const branch = { merged: false, name: 'issue-100', updatedDate: new Date(), mergedDate: new Date(), hasUncommittedChanges: false, }; it('should output command with branch name', async(): Promise<void> => { const command = 'echo {branch}'; const commandToExec = `echo ${branch.name}`; const builder = new BranchRemoverOptionsBuilder(); const options = builder .afterRemove(command) .build(); await options.afterRemove({ branch, context }); const [ commandPreparation, commandResult, ] = parseLog(); expect(commandPreparation.source).to.be.equal(command); expect(commandPreparation.parsed).to.be.equal(commandToExec); expect(commandResult.command).to.be.equal(commandToExec); expect(commandResult.stdout).to.be.equal(branch.name); expect(commandResult.stderr).to.be.empty; }); it('should be thrown an exception', (): void => { const command = 'exit 1'; const builder = new BranchRemoverOptionsBuilder(); const options = builder .afterRemove(command) .build(); expect( async(): Promise<void> => { await options.afterRemove({ branch, context }); } ).to.be.throw; }); }); describe('cachePath', (): void => { afterEach((): void => { sinon.restore(); }); it('should have a default path for the cache file', (): void => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); sinon.stub(fs, 'existsSync'); options.cache.provider.load(); sinon.assert.calledWith( <any>fs.existsSync, BranchRemoverOptionsBuilder.DEFAULT_CACHE_PATH ); }); it('should have custom path to the cache file', (): void => { const cachePath = './custom.cache'; const builder = new BranchRemoverOptionsBuilder(); const options = builder .cachePath(cachePath) .build(); sinon.stub(fs, 'existsSync'); options.cache.provider.load(); sinon.assert.calledWith( <any>fs.existsSync, cachePath ); }); it('should throw an exception if an empty path is specified', (): void => { expect( (): void => { const builder = new BranchRemoverOptionsBuilder(); builder.cachePath(''); } ).to.be.throw('Parameter "path" cannot be null or empty.'); }); }); describe('cacheTimeout', (): void => { afterEach((): void => { sinon.restore(); }); it('should be 0 by default', (): void => { const builder = new BranchRemoverOptionsBuilder(); const options = builder.build(); expect(options.cache.timeout).to.be.equal(0); }); it('should have the specified value', (): void => { const builder = new BranchRemoverOptionsBuilder(); const options = builder .cacheTimeout(300) .build(); expect(options.cache.timeout).to.be.equal(300); }); }); describe('debug', (): void => { it('should contain debug information', async(): Promise<void> => { const logger = new Logger({ levels: Logger.defaultLevels, level: 'debug', transports: [ new winston.transports.Stream({ stream: writer, }), ], }); context = { logger, }; const options = new BranchRemoverOptionsBuilder() .debug() .quiet() .build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; await remove({ context, branch: { merged: true, name: 'issue-100', updatedDate: new Date(), hasUncommittedChanges: false, }, }); expect(writer.toString()).to.be.contain('"level":"debug"'); }); }); it('should not contain debug information', async(): Promise<void> => { const logger = new Logger({ levels: Logger.defaultLevels, level: 'info', transports: [ new winston.transports.Stream({ stream: writer, }), ], }); context = { logger, }; const options = new BranchRemoverOptionsBuilder() .debug() .quiet() .build(); const remove = options.remove as BranchRemoverOptionsRemoveFunction; await remove({ context, branch: { merged: true, name: 'issue-100', updatedDate: new Date(), hasUncommittedChanges: false, }, }); expect(writer.toString()).to.be.not.contain('"level":"debug"'); }); }); });