diff --git a/package.json b/package.json index 4b1c6d15..842b446b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,11 @@ "command": "git-graph.clearAvatarCache", "title": "Clear Avatar Cache" }, + { + "category": "Git Graph", + "command": "git-graph.clearCICDCache", + "title": "Clear CI/CD Status Cache" + }, { "category": "Git Graph", "command": "git-graph.endAllWorkspaceCodeReviews", @@ -942,6 +947,11 @@ "default": false, "description": "Fetch avatars of commit authors and committers. By enabling this setting, you consent to commit author and committer email addresses being sent GitHub, GitLab or Gravatar, depending on the repositories remote origin." }, + "git-graph.repository.commits.fetchCICDsMaximumStatuses": { + "type": "number", + "default": 1000, + "description": "Specifies the number of CI/CD fetch maximum statuses." + }, "git-graph.repository.commits.initialLoad": { "type": "number", "default": 300, diff --git a/src/cicdManager.ts b/src/cicdManager.ts new file mode 100644 index 00000000..ab0beb72 --- /dev/null +++ b/src/cicdManager.ts @@ -0,0 +1,817 @@ +// import * as crypto from 'crypto'; +import { IncomingMessage } from 'http'; +import * as https from 'https'; +import * as http from 'http'; +import { getConfig } from './config'; +// import * as url from 'url'; +import { ExtensionState } from './extensionState'; +import { Logger } from './logger'; +import { CICDConfig, CICDData, CICDDataSave, CICDProvider } from './types'; +import { Disposable, toDisposable } from './utils/disposable'; +import { EventEmitter } from './utils/event'; +import { RepoManager } from './repoManager'; + +/** + * Manages fetching and caching CICDs. + */ +export class CicdManager extends Disposable { + private readonly extensionState: ExtensionState; + private readonly logger: Logger; + private readonly cicdEventEmitter: EventEmitter; + private readonly repoManager: RepoManager; + + private cicds: CICDCache; + private queue: CicdRequestQueue; + private interval: NodeJS.Timer | null = null; + + private gitHubTimeout: number = 0; + private gitLabTimeout: number = 0; + private jenkinsTimeout: number = 0; + + private per_page: number = 100; + + /** + * Creates the Git Graph CICD Manager. + * @param extensionState The Git Graph ExtensionState instance. + * @param logger The Git Graph Logger instance. + */ + constructor(extensionState: ExtensionState, repoManager: RepoManager, logger: Logger) { + super(); + this.extensionState = extensionState; + this.repoManager = repoManager; + this.logger = logger; + this.cicdEventEmitter = new EventEmitter(); + this.cicds = this.extensionState.getCICDCache(); + this.queue = new CicdRequestQueue(() => { + if (this.interval !== null) return; + this.interval = setInterval(() => { + // Fetch cicds every 1 seconds + this.fetchCICDsInterval(); + }, 1000); + this.fetchCICDsInterval(); + }); + + this.registerDisposables( + // Stop fetching cicds when disposed + toDisposable(() => { + this.stopInterval(); + }), + + // Dispose the cicd event emitter + this.cicdEventEmitter + ); + } + + /** + * Stops the interval used to fetch cicds. + */ + private stopInterval() { + if (this.interval !== null) { + clearInterval(this.interval); + this.interval = null; + } + } + + /** + * Get the data of an cicd. + * @param repo The repository that the cicd is used in. + * @param hash The hash identifying the cicd commit. + * @param cicdConfigs The CICDConfigs. + * @returns A JSON encoded data of an cicd if the cicd exists, otherwise NULL. + */ + public getCICDDetail(repo: string, hash: string) { + return new Promise((resolve) => { + let repos = this.repoManager.getRepos(); + if (typeof repos[repo] !== 'undefined') { + let cicdConfigs = repos[repo].cicdConfigs; + if (cicdConfigs !== null) { + cicdConfigs.forEach(cicdConfig => { + this.queue.add(repo, cicdConfig, -1, true, true, hash); + }); + } + } + if (typeof this.cicds[repo] !== 'undefined' && typeof this.cicds[repo][hash] !== 'undefined' && this.cicds[repo][hash] !== null) { + resolve(JSON.stringify(this.cicds[repo][hash])); + } else { + resolve(null); + } + }); + } + + /** + * Get the Event that can be used to subscribe to receive requested cicds. + * @returns The Event. + */ + get onCICD() { + return this.cicdEventEmitter.subscribe; + } + + /** + * Remove all cicds from the cache. + */ + public clearCache() { + this.cicds = {}; + this.extensionState.clearCICDCache(); + } + + /** + * Triggered by an interval to fetch cicds from GitHub and GitLab. + */ + private async fetchCICDsInterval() { + if (this.queue.hasItems()) { + + let cicdRequest = this.queue.takeItem(); + if (cicdRequest === null) return; // No cicd can be checked at the current time + + switch (cicdRequest.cicdConfig.provider) { + case CICDProvider.GitHubV3: + this.fetchFromGitHub(cicdRequest); + break; + case CICDProvider.GitLabV4: + this.fetchFromGitLab(cicdRequest); + break; + case CICDProvider.JenkinsV2: + this.fetchFromJenkins(cicdRequest); + break; + default: + this.logger.log('Unknown provider Error'); + break; + } + } else { + // Stop the interval if there are no items remaining in the queue + this.stopInterval(); + } + } + + /** + * Fetch an cicd from GitHub. + * @param cicdRequest The cicd request to fetch. + */ + private fetchFromGitHub(cicdRequest: CICDRequestItem) { + let t = (new Date()).getTime(); + if (t < this.gitHubTimeout) { + // Defer request until after timeout + this.queue.addItem(cicdRequest, this.gitHubTimeout, false); + this.fetchCICDsInterval(); + return; + } + + let cicdConfig = cicdRequest.cicdConfig; + + let gitUrl = cicdConfig.cicdUrl.replace(/\/$/g, ''); + gitUrl = gitUrl.replace(/.git$/g, ''); + const match1 = gitUrl.match(/^(.+?):\/\/(.+?):?(\d+)?(\/.*)?$/); + let hostProtocol = (match1 !== null && typeof match1[1] !== 'undefined') ? '' + match1[1].toLowerCase() : ''; + let hostRootUrl = (match1 !== null && typeof match1[2] !== 'undefined') ? 'api.' + match1[2] : ''; + let hostPort = (match1 !== null && typeof match1[3] !== 'undefined') ? '' + match1[3] : ''; + let hostPath = (match1 !== null && typeof match1[4] !== 'undefined') ? '' + match1[4].replace(/^\//, '') : ''; + + // https://docs.github.com/en/rest/reference/actions#list-workflow-runs-for-a-repository + let cicdRootPath = `/repos/${hostPath}/actions/runs?per_page=${this.per_page}`; + if (cicdRequest.detail) { + // https://docs.github.com/en/rest/reference/checks#list-check-runs-for-a-git-reference + cicdRootPath = `/repos/${hostPath}/commits/${cicdRequest.hash}/check-runs?per_page=${this.per_page}`; + } + if (cicdRequest.page > 1) { + cicdRootPath = `${cicdRootPath}&page=${cicdRequest.page}`; + } + + let headers: any = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'vscode-git-graph' + }; + if (cicdConfig.cicdToken !== '') { + headers['Authorization'] = `token ${cicdConfig.cicdToken}`; + } + + let triggeredOnError = false; + const onError = (err: Error) => { + this.logger.log('GitHub API ' + hostProtocol + ' Error - ' + err?.message); + if (!triggeredOnError) { + // If an error occurs, try again after 5 minutes + triggeredOnError = true; + this.gitHubTimeout = t + 300000; + this.queue.addItem(cicdRequest, this.gitHubTimeout, false); + } + }; + if (!(hostProtocol === 'http' || hostProtocol === 'https') || hostRootUrl === '') { + this.logger.log('Requesting CICD is not match URL (' + cicdConfig.cicdUrl + ') for GitHub'); + } else { + this.logger.log('Requesting CICD for ' + hostProtocol + '://' + hostRootUrl + cicdRootPath + ' detail=' + cicdRequest.detail + ' page=' + cicdRequest.page + ' from GitHub'); + (hostProtocol === 'http' ? http : https).get({ + hostname: hostRootUrl, path: cicdRootPath, + port: hostPort, + headers: headers, + agent: false, timeout: 15000 + }, (res) => { + let respBody = ''; + res.on('data', (chunk: Buffer) => { respBody += chunk; }); + res.on('end', async () => { + if (res.headers['x-ratelimit-remaining'] === '0') { + // If the GitHub Api rate limit was reached, store the github timeout to prevent subsequent requests + this.gitHubTimeout = parseInt(res.headers['x-ratelimit-reset']) * 1000; + this.logger.log('GitHub API Rate Limit Reached - Paused fetching from GitHub until the Rate Limit is reset (RateLimit=' + res.headers['x-ratelimit-limit'] + '(1 hour)/' + new Date(this.gitHubTimeout).toString() + ')'); + if (cicdRequest.cicdConfig.cicdToken === '') { + this.logger.log('GitHub API Rate Limit can upgrade by Access Token.'); + } + } + + if (res.statusCode === 200) { // Success + this.logger.log('GitHub API - (' + res.statusCode + ')' + hostProtocol + '://' + hostRootUrl + cicdRootPath); + try { + let respJson: any = JSON.parse(respBody); + if (typeof respJson['check_runs'] !== 'undefined' && respJson['check_runs'].length >= 1) { // url found + let ret: CICDData[] = respJson['check_runs'].map((elm: { [x: string]: any; }) => { + return { + id: elm['id'], + status: elm['conclusion'] === null ? elm['status'] : elm['conclusion'], + ref: '', + sha: elm['head_sha'], + web_url: elm['html_url'], + created_at: elm['created_at'], + updated_at: elm['updated_at'], + name: elm['app']!['name'] + '(' + elm['name'] + ')', + event: '', + detail: cicdRequest.detail + }; + }); + ret.forEach(element => { + let save = this.convCICDData2CICDDataSave(element); + this.saveCICD(cicdRequest.repo, element.sha, element.id, save); + }); + this.reFetchPageGitHub(cicdRequest, res, cicdConfig); + return; + } else if (typeof respJson['workflow_runs'] !== 'undefined' && respJson['workflow_runs'].length >= 1) { // url found + let ret: CICDData[] = respJson['workflow_runs'].map((elm: { [x: string]: any; }) => { + return { + id: elm['id'], + status: elm['conclusion'] === null ? elm['status'] : elm['conclusion'], + ref: elm['head_branch'], + sha: elm['head_sha'], + web_url: elm['html_url'], + created_at: elm['created_at'], + updated_at: elm['updated_at'], + name: elm['name'], + event: elm['event'], + detail: cicdRequest.detail + }; + }); + ret.forEach(element => { + let save = this.convCICDData2CICDDataSave(element); + this.saveCICD(cicdRequest.repo, element.sha, element.id, save); + }); + this.reFetchPageGitHub(cicdRequest, res, cicdConfig); + return; + } + } catch (e) { + this.logger.log('GitHub API Error - (' + res.statusCode + ')API Result error. : ' + e.message); + } + return; + } else if (res.statusCode === 403) { + // Rate limit reached, try again after timeout + this.queue.addItem(cicdRequest, this.gitHubTimeout, false); + return; + } else if (res.statusCode === 422 && cicdRequest.attempts < 4) { + // Commit not found on remote, try again with the next commit if less than 5 attempts have been made + this.queue.addItem(cicdRequest, 0, true); + return; + } else if (res.statusCode! >= 500) { + // If server error, try again after 10 minutes + this.gitHubTimeout = t + 600000; + this.queue.addItem(cicdRequest, this.gitHubTimeout, false); + return; + } else { + // API Error + try { + let respJson: any = JSON.parse(respBody); + if (typeof respJson.message === 'undefined') { + throw new Error('message is undefined!'); + } + this.logger.log('GitHub API Error - (' + res.statusCode + ')' + respJson.message); + } catch (e) { + this.logger.log('GitHub API Error - (' + res.statusCode + ')' + res.statusMessage); + } + } + }); + res.on('error', onError); + }).on('error', onError); + } + } + + /** + * ReFetch an cicd from GitHub. + * @param cicdRequest The cicd request to fetch. + * @param res The IncomingMessage. + * @param cicdConfig The CICDConfig. + */ + private reFetchPageGitHub(cicdRequest: CICDRequestItem, res: IncomingMessage, cicdConfig: CICDConfig) { + if (cicdRequest.page === -1) { + let last = 1; + if (typeof res.headers['link'] === 'string') { + const DELIM_LINKS = ','; + const DELIM_LINK_PARAM = ';'; + let links = res.headers['link'].split(DELIM_LINKS); + links.forEach(link => { + let segments = link.split(DELIM_LINK_PARAM); + + let linkPart = segments[0].trim(); + if (!linkPart.startsWith('<') || !linkPart.endsWith('>')) { + return true; + } + linkPart = linkPart.substring(1, linkPart.length - 1); + let match3 = linkPart.match(/&page=(\d+).*$/); + let linkPage = match3 !== null ? match3[1] : '0'; + + for (let i = 1; i < segments.length; i++) { + let rel = segments[i].trim().split('='); + if (rel.length < 2) { + continue; + } + + let relValue = rel[1]; + if (relValue.startsWith('"') && relValue.endsWith('"')) { + relValue = relValue.substring(1, relValue.length - 1); + } + + if (relValue === 'last') { + last = parseInt(linkPage); + } + } + }); + } + if (last > Math.ceil(cicdRequest.maximumStatuses / this.per_page)) { + last = Math.ceil(cicdRequest.maximumStatuses / this.per_page); + this.logger.log('CICD Maximum Statuses(maximumStatuses=' + cicdRequest.maximumStatuses + ') reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + } + + this.logger.log('Added CICD for ' + cicdConfig.cicdUrl + ' last_page=' + last + '(RateLimit=' + (res.headers['x-ratelimit-limit'] || 'None') + '(1 hour)/Remaining=' + (res.headers['x-ratelimit-remaining'] || 'None') + (res.headers['x-ratelimit-reset'] ? '/' + new Date(parseInt(res.headers['x-ratelimit-reset']) * 1000).toString() : '') + ') from GitHub'); + for (let i = 1; i < last; i++) { + this.queue.add(cicdRequest.repo, cicdRequest.cicdConfig, i + 1, true, cicdRequest.detail, cicdRequest.hash); + } + } + } + + /** + * Fetch an cicd from GitLab. + * @param cicdRequest The cicd request to fetch. + */ + private fetchFromGitLab(cicdRequest: CICDRequestItem) { + let t = (new Date()).getTime(); + if (t < this.gitLabTimeout) { + // Defer request until after timeout + this.queue.addItem(cicdRequest, this.gitLabTimeout, false); + this.fetchCICDsInterval(); + return; + } + + let cicdConfig = cicdRequest.cicdConfig; + + let gitUrl = cicdConfig.cicdUrl.replace(/\/$/g, ''); + gitUrl = gitUrl.replace(/.git$/g, ''); + const match1 = gitUrl.match(/^(.+?):\/\/(.+?):?(\d+)?(\/.*)?$/); + let hostProtocol = (match1 !== null && typeof match1[1] !== 'undefined') ? '' + match1[1].toLowerCase() : ''; + let hostRootUrl = (match1 !== null && typeof match1[2] !== 'undefined') ? '' + match1[2] : ''; + let hostPort = (match1 !== null && typeof match1[3] !== 'undefined') ? '' + match1[3] : ''; + let hostPath = (match1 !== null && typeof match1[4] !== 'undefined') ? '' + match1[4].replace(/^\//, '').replace(/\//g, '%2F') : ''; + + // Pipelines API https://docs.gitlab.com/ee/api/pipelines.html#list-project-pipelines + let cicdRootPath = `/api/v4/projects/${hostPath}/pipelines?per_page=${this.per_page}`; + if (cicdRequest.detail) { + // Commits API https://docs.gitlab.com/ee/api/commits.html#list-the-statuses-of-a-commit + cicdRootPath = `/api/v4/projects/${hostPath}/repository/commits/${cicdRequest.hash}/statuses?per_page=${this.per_page}`; + } + if (cicdRequest.page > 1) { + cicdRootPath = `${cicdRootPath}&page=${cicdRequest.page}`; + } + + let headers: any = { + 'User-Agent': 'vscode-git-graph' + }; + if (cicdConfig.cicdToken !== '') { + headers['PRIVATE-TOKEN'] = cicdConfig.cicdToken; + } + + let triggeredOnError = false; + const onError = (err: Error) => { + this.logger.log('GitLab API ' + hostProtocol + ' Error - ' + err?.message); + if (!triggeredOnError) { + // If an error occurs, try again after 5 minutes + triggeredOnError = true; + this.gitLabTimeout = t + 300000; + this.queue.addItem(cicdRequest, this.gitLabTimeout, false); + } + }; + + if (!(hostProtocol === 'http' || hostProtocol === 'https') || hostRootUrl === '') { + this.logger.log('Requesting CICD is not match URL (' + cicdConfig.cicdUrl + ') for GitLab'); + } else { + this.logger.log('Requesting CICD for ' + hostProtocol + '://' + hostRootUrl + cicdRootPath + ' detail=' + cicdRequest.detail + ' page=' + cicdRequest.page + ' from GitLab'); + (hostProtocol === 'http' ? http : https).get({ + hostname: hostRootUrl, path: cicdRootPath, + port: hostPort, + headers: headers, + agent: false, timeout: 15000 + }, (res) => { + let respBody = ''; + res.on('data', (chunk: Buffer) => { respBody += chunk; }); + res.on('end', async () => { + if (res.headers['ratelimit-remaining'] === '0') { + // If the GitLab Api rate limit was reached, store the gitlab timeout to prevent subsequent requests + this.gitLabTimeout = parseInt(res.headers['ratelimit-reset']) * 1000; + this.logger.log('GitLab API Rate Limit Reached - Paused fetching from GitLab until the Rate Limit is reset (RateLimit=' + res.headers['ratelimit-limit'] + '(every minute)/' + new Date(this.gitLabTimeout).toString() + ')'); + } + + if (res.statusCode === 200) { // Success + try { + this.logger.log('GitLab API - (' + res.statusCode + ')' + hostProtocol + '://' + hostRootUrl + cicdRootPath); + if (typeof res.headers['x-page'] === 'string' && typeof res.headers['x-total-pages'] === 'string' && typeof res.headers['x-total'] === 'string') { + let respJson: any = JSON.parse(respBody); + if (parseInt(res.headers['x-total']) !== 0 && respJson.length && respJson[0].id) { // url found + let ret: CICDData[] = respJson; + ret.forEach(element => { + let save: CICDDataSave; + if (cicdRequest.detail) { + save = this.convGitLabComitStatuses2CICDDataSave(element, cicdRequest.detail, + `${hostProtocol}://${hostRootUrl}${hostPort === '' ? '' : ':' + hostPort}/${hostPath.replace(/%2F/g, '/')}/-/jobs/`); + } else { + save = this.convCICDData2CICDDataSave( + Object.assign({}, + { + name: '', + ref: '', + web_url: '', + event: '', + detail: cicdRequest.detail + }, + element + )); + } + this.saveCICD(cicdRequest.repo, element.sha, element.id, save); + }); + } + + if (cicdRequest.page === -1) { + let last = parseInt(res.headers['x-total-pages']); + if (last > Math.ceil(cicdRequest.maximumStatuses / this.per_page)) { + last = Math.ceil(cicdRequest.maximumStatuses / this.per_page); + this.logger.log('CICD Maximum Statuses(maximumStatuses=' + cicdRequest.maximumStatuses + ') reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + } + + this.logger.log('Added CICD for ' + cicdConfig.cicdUrl + ' last_page=' + last + '(RateLimit=' + (res.headers['ratelimit-limit'] || 'None') + '(every minute)/Remaining=' + (res.headers['ratelimit-remaining'] || 'None') + (res.headers['ratelimit-reset'] ? '/' + new Date(parseInt(res.headers['ratelimit-reset']) * 1000).toString() : '') + ') from GitLab'); + for (let i = 1; i < last; i++) { + this.queue.add(cicdRequest.repo, cicdRequest.cicdConfig, i + 1, true, cicdRequest.detail, cicdRequest.hash); + } + } + return; + } + } catch (e) { + this.logger.log('GitLab API Error - (' + res.statusCode + ')API Result error. : ' + e.message); + } + } else if (res.statusCode === 429) { + // Rate limit reached, try again after timeout + this.queue.addItem(cicdRequest, this.gitLabTimeout, false); + return; + } else if (res.statusCode! >= 500) { + // If server error, try again after 10 minutes + this.gitLabTimeout = t + 600000; + this.queue.addItem(cicdRequest, this.gitLabTimeout, false); + return; + } else { + // API Error + try { + let respJson: any = JSON.parse(respBody); + if (typeof respJson.message === 'undefined') { + throw new Error('message is undefined!'); + } + this.logger.log('GitLab API Error - (' + res.statusCode + ')' + respJson.message); + } catch (e) { + this.logger.log('GitLab API Error - (' + res.statusCode + ')' + res.statusMessage); + } + } + }); + res.on('error', onError); + }).on('error', onError); + } + } + + /** + * Fetch an cicd from Jenkins. + * @param cicdRequest The cicd request to fetch. + */ + private fetchFromJenkins(cicdRequest: CICDRequestItem) { + // if (cicdRequest.detail) { + // return; + // } + let t = (new Date()).getTime(); + if (t < this.jenkinsTimeout) { + // Defer request until after timeout + this.queue.addItem(cicdRequest, this.jenkinsTimeout, false); + this.fetchCICDsInterval(); + return; + } + + let cicdConfig = cicdRequest.cicdConfig; + + let gitUrl = cicdConfig.cicdUrl.replace(/\/$/g, ''); + gitUrl = gitUrl.replace(/.git$/g, ''); + const match1 = gitUrl.match(/^(.+?):\/\/(.+?):?(\d+)?(\/.*)?$/); + let hostProtocol = (match1 !== null && typeof match1[1] !== 'undefined') ? '' + match1[1].toLowerCase() : ''; + let hostRootUrl = (match1 !== null && typeof match1[2] !== 'undefined') ? '' + match1[2] : ''; + let hostPort = (match1 !== null && typeof match1[3] !== 'undefined') ? '' + match1[3] : ''; + let hostPath = (match1 !== null && typeof match1[4] !== 'undefined') ? '' + match1[4].replace(/^\//, '') : ''; + + // https://www.jenkins.io/doc/book/using/remote-access-api/ + let cicdRootPath = `/${hostPath}/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]`; + + let headers: any = { + 'User-Agent': 'vscode-git-graph' + }; + if (cicdConfig.cicdToken !== '') { + // headers['Authorization'] = 'Basic ' + new Buffer(username + ':' + passw).toString('base64'); + headers['Authorization'] = 'Basic ' + new Buffer(cicdConfig.cicdToken).toString('base64'); + } + + let triggeredOnError = false; + const onError = (err: Error) => { + this.logger.log('Jenkins API ' + hostProtocol + ' Error - ' + err?.message); + if (!triggeredOnError) { + // If an error occurs, try again after 5 minutes + triggeredOnError = true; + this.jenkinsTimeout = t + 300000; + this.queue.addItem(cicdRequest, this.jenkinsTimeout, false); + } + }; + if (!(hostProtocol === 'http' || hostProtocol === 'https') || hostRootUrl === '') { + this.logger.log('Requesting CICD is not match URL (' + cicdConfig.cicdUrl + ') for Jenkins'); + } else { + this.logger.log('Requesting CICD for ' + hostProtocol + '://' + hostRootUrl + cicdRootPath + ' page=' + cicdRequest.page + ' from Jenkins'); + (hostProtocol === 'http' ? http : https).get({ + hostname: hostRootUrl, path: cicdRootPath, + port: hostPort, + headers: headers, + agent: false, timeout: 15000 + }, (res) => { + let respBody = ''; + res.on('data', (chunk: Buffer) => { respBody += chunk; }); + res.on('end', async () => { + if (res.headers['ratelimit-remaining'] === '0') { + // If the Jenkins Api rate limit was reached, store the Jenkins timeout to prevent subsequent requests + this.jenkinsTimeout = parseInt(res.headers['ratelimit-reset']) * 1000; + this.logger.log('Jenkins API Rate Limit Reached - Paused fetching from Jenkins until the Rate Limit is reset (RateLimit=' + res.headers['ratelimit-limit'] + '(every minute)/' + new Date(this.jenkinsTimeout).toString() + ')'); + } + + if (res.statusCode === 200) { // Success + this.logger.log('Jenkins API - (' + res.statusCode + ')' + hostProtocol + '://' + hostRootUrl + cicdRootPath); + try { + if (typeof res.headers['x-jenkins'] === 'string' && res.headers['x-jenkins'].startsWith('2.')) { + let respJson: any = JSON.parse(respBody); + if (respJson['builds'].length) { // url found + let ret: CICDData[] = []; + respJson['builds'].forEach((elmBuild: { [x: string]: any; }) => { + elmBuild['actions'].forEach((elmAction: { [x: string]: any; }) => { + if (typeof elmAction['lastBuiltRevision'] !== 'undefined' && typeof elmAction['lastBuiltRevision']['branch'] !== 'undefined' + && typeof elmAction['lastBuiltRevision']['branch'][0] !== 'undefined' && typeof elmAction['lastBuiltRevision']['branch'][0].SHA1 !== 'undefined') { + ret.push( + { + id: elmBuild['id'], + status: elmBuild['result'], + ref: elmAction['lastBuiltRevision']!['branch']![0]!.name, + sha: elmAction['lastBuiltRevision']!['branch']![0]!.SHA1, + web_url: elmBuild['url'] || '', + created_at: '', + updated_at: elmBuild['timestamp'], + name: elmBuild['fullDisplayName'], + event: '', + detail: cicdRequest.detail + } + ); + } + }); + }); + ret.forEach(element => { + let save = this.convCICDData2CICDDataSave(element); + this.saveCICD(cicdRequest.repo, element.sha, element.id, save); + }); + return; + } + } + } catch (e) { + this.logger.log('Jenkins API Error - (' + res.statusCode + ')API Result error. : ' + e.message); + } + } else if (res.statusCode === 429) { + // Rate limit reached, try again after timeout + this.queue.addItem(cicdRequest, this.jenkinsTimeout, false); + return; + } else if (res.statusCode! >= 500) { + // If server error, try again after 10 minutes + this.jenkinsTimeout = t + 600000; + this.queue.addItem(cicdRequest, this.jenkinsTimeout, false); + return; + } else { + // API Error + try { + let respJson: any = JSON.parse(respBody); + if (typeof respJson.message === 'undefined') { + throw new Error('message is undefined!'); + } + this.logger.log('Jenkins API Error - (' + res.statusCode + ')' + respJson.message); + } catch (e) { + this.logger.log('Jenkins API Error - (' + res.statusCode + ')' + res.statusMessage); + } + } + }); + res.on('error', onError); + }).on('error', onError); + } + } + + /** + * Fetch an cicd from GitHub/GitLab/Jenkins. + * @param cicdData The CICDData. + * @returns The CICDDataSave. + */ + private convCICDData2CICDDataSave(cicdData: CICDData): CICDDataSave { + return { + name: cicdData!.name, + ref: cicdData!.ref, + status: cicdData!.status, + web_url: cicdData!.web_url, + event: cicdData!.event, + detail: cicdData!.detail, + allow_failure: false + }; + } + + /** + * Fetch an cicd from GitLab. + * @param data The result of GitLab commit statuses API. + * @param detail Detail fetch flag. + * @returns The CICDDataSave. + */ + private convGitLabComitStatuses2CICDDataSave(data: any, detail: boolean, url: string): CICDDataSave { + let ret = { + name: data!.name, + ref: data!.ref, + status: data!.status, + web_url: data!.target_url, + event: '', + detail: detail, + allow_failure: data!.allow_failure + }; + if (typeof ret.web_url === 'undefined' || ret.web_url === null) { + ret.web_url = url + data.id; + } + return ret; + } + + /** + * Emit an CICDEvent to any listeners. + * @param repo The repository that the cicd is used in. + * @param hash The hash identifying the cicd commit. + * @param cicdDataSaves The hash of CICDDataSave. + * @returns A promise indicating if the event was emitted successfully. + */ + private emitCICD(repo: string, hash: string, cicdDataSaves: { [id: string]: CICDDataSave }) { + if (this.cicdEventEmitter.hasSubscribers()) { + this.cicdEventEmitter.emit({ + repo: repo, + hash: hash, + cicdDataSaves: cicdDataSaves + }); + } + } + + /** + * Save an cicd in the cache. + * @param repo The repository that the cicd is used in. + * @param hash The hash identifying the cicd commit. + * @param id The identifying the cicdDataSave. + * @param cicdDataSave The CICDDataSave. + */ + private saveCICD(repo: string, hash: string, id: string, cicdDataSave: CICDDataSave) { + if (typeof this.cicds[repo] === 'undefined') { + this.cicds[repo] = {}; + } + if (typeof this.cicds[repo][hash] === 'undefined') { + this.cicds[repo][hash] = {}; + } + this.cicds[repo][hash][id] = cicdDataSave; + this.extensionState.saveCICD(repo, hash, id, cicdDataSave); + // this.logger.log('Saved CICD for ' + cicdData.sha); + this.emitCICD(repo, hash, this.cicds[repo][hash]); + } +} + +/** + * Represents a queue of cicd requests, ordered by their `checkAfter` value. + */ +class CicdRequestQueue { + private queue: CICDRequestItem[] = []; + private itemsAvailableCallback: () => void; + + /** + * Create an CICD Request Queue. + * @param itemsAvailableCallback A callback that is invoked when the queue transitions from having no items, to having at least one item. + */ + constructor(itemsAvailableCallback: () => void) { + this.itemsAvailableCallback = itemsAvailableCallback; + } + + /** + * Create and add a new cicd request to the queue. + * @param repo The repository that the cicd is used in. + * @param cicdConfig The CICDConfig. + * @param page The page of cicd request. + * @param immediate Whether the avatar should be fetched immediately. + * @param detail Flag of fetch detail. + * @param hash hash for fetch detail. + */ + public add(repo: string, cicdConfig: CICDConfig, page: number, immediate: boolean, detail: boolean, hash: string) { + const existingRequest = this.queue.find((request) => request.cicdConfig.cicdUrl === cicdConfig.cicdUrl && request.page === page && request.detail === detail && request.hash === hash); + if (!existingRequest) { + const config = getConfig(); + this.insertItem({ + repo: repo, + cicdConfig: cicdConfig, + page: page, + checkAfter: immediate || this.queue.length === 0 + ? 0 + : this.queue[this.queue.length - 1].checkAfter + 1, + attempts: 0, + detail: detail, + hash: hash, + maximumStatuses: config.fetchCICDsMaximumStatuses + }); + } + } + + /** + * Add an existing cicd request item back onto the queue. + * @param item The cicd request item. + * @param checkAfter The earliest time the cicd should be requested. + * @param failedAttempt Did the fetch attempt fail. + */ + public addItem(item: CICDRequestItem, checkAfter: number, failedAttempt: boolean) { + item.checkAfter = checkAfter; + if (failedAttempt) item.attempts++; + this.insertItem(item); + } + + /** + * Check if there are items in the queue. + * @returns TRUE => Items in the queue, FALSE => Queue is empty. + */ + public hasItems() { + return this.queue.length > 0; + } + + /** + * Take the next item from the queue if an item is available. + * @returns An cicd request item, or NULL if no item is available. + */ + public takeItem() { + if (this.queue.length > 0 && this.queue[0].checkAfter < (new Date()).getTime()) return this.queue.shift()!; + return null; + } + + /** + * Insert an cicd request item into the queue. + * @param item The cicd request item. + */ + private insertItem(item: CICDRequestItem) { + let l = 0, r = this.queue.length - 1, c, prevLength = this.queue.length; + while (l <= r) { + c = l + r >> 1; + if (this.queue[c].checkAfter <= item.checkAfter) { + l = c + 1; + } else { + r = c - 1; + } + } + this.queue.splice(l, 0, item); + if (prevLength === 0) this.itemsAvailableCallback(); + } +} + + +export type CICDCache = { [repo: string]: { [hash: string]: { [id: string]: CICDDataSave } } }; + + +// Request item to CicdRequestQueue +interface CICDRequestItem { + repo: string; + cicdConfig: CICDConfig; + page: number; + checkAfter: number; + attempts: number; + detail: boolean; + hash: string; + maximumStatuses: number; +} + +// Event to GitGraphView +export interface CICDEvent { + repo: string; + hash: string; + cicdDataSaves: { [id: string]: CICDDataSave }; +} diff --git a/src/commands.ts b/src/commands.ts index a6474a68..88ab632e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,6 +1,7 @@ import * as os from 'os'; import * as vscode from 'vscode'; import { AvatarManager } from './avatarManager'; +import { CicdManager } from './cicdManager'; import { getConfig } from './config'; import { DataSource } from './dataSource'; import { DiffDocProvider, decodeDiffDocUri } from './diffDocProvider'; @@ -18,6 +19,7 @@ import { Event } from './utils/event'; export class CommandManager extends Disposable { private readonly context: vscode.ExtensionContext; private readonly avatarManager: AvatarManager; + private readonly cicdManager: CicdManager; private readonly dataSource: DataSource; private readonly extensionState: ExtensionState; private readonly logger: Logger; @@ -28,6 +30,7 @@ export class CommandManager extends Disposable { * Creates the Git Graph Command Manager. * @param extensionPath The absolute file path of the directory containing the extension. * @param avatarManger The Git Graph AvatarManager instance. + * @param cicdManager The Git Graph CicdManager instance. * @param dataSource The Git Graph DataSource instance. * @param extensionState The Git Graph ExtensionState instance. * @param repoManager The Git Graph RepoManager instance. @@ -35,10 +38,11 @@ export class CommandManager extends Disposable { * @param onDidChangeGitExecutable The Event emitting the Git executable for Git Graph to use. * @param logger The Git Graph Logger instance. */ - constructor(context: vscode.ExtensionContext, avatarManger: AvatarManager, dataSource: DataSource, extensionState: ExtensionState, repoManager: RepoManager, gitExecutable: GitExecutable | null, onDidChangeGitExecutable: Event, logger: Logger) { + constructor(context: vscode.ExtensionContext, avatarManger: AvatarManager, cicdManager: CicdManager, dataSource: DataSource, extensionState: ExtensionState, repoManager: RepoManager, gitExecutable: GitExecutable | null, onDidChangeGitExecutable: Event, logger: Logger) { super(); this.context = context; this.avatarManager = avatarManger; + this.cicdManager = cicdManager; this.dataSource = dataSource; this.extensionState = extensionState; this.logger = logger; @@ -50,6 +54,7 @@ export class CommandManager extends Disposable { this.registerCommand('git-graph.addGitRepository', () => this.addGitRepository()); this.registerCommand('git-graph.removeGitRepository', () => this.removeGitRepository()); this.registerCommand('git-graph.clearAvatarCache', () => this.clearAvatarCache()); + this.registerCommand('git-graph.clearCICDCache', () => this.clearCICDCache()); this.registerCommand('git-graph.fetch', () => this.fetch()); this.registerCommand('git-graph.endAllWorkspaceCodeReviews', () => this.endAllWorkspaceCodeReviews()); this.registerCommand('git-graph.endSpecificWorkspaceCodeReview', () => this.endSpecificWorkspaceCodeReview()); @@ -120,7 +125,7 @@ export class CommandManager extends Disposable { loadRepo = this.repoManager.getRepoContainingFile(getPathFromUri(vscode.window.activeTextEditor.document.uri)); } - GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.repoManager, this.logger, loadRepo !== null ? { repo: loadRepo } : null); + GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.cicdManager, this.repoManager, this.logger, loadRepo !== null ? { repo: loadRepo } : null); } /** @@ -194,6 +199,13 @@ export class CommandManager extends Disposable { }); } + /** + * The method run when the `git-graph.clearCICDCache` command is invoked. + */ + private clearCICDCache() { + this.cicdManager.clearCache(); + } + /** * The method run when the `git-graph.fetch` command is invoked. */ @@ -221,7 +233,7 @@ export class CommandManager extends Disposable { canPickMany: false }).then((item) => { if (item && item.description) { - GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.repoManager, this.logger, { + GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.cicdManager, this.repoManager, this.logger, { repo: item.description, runCommandOnLoad: 'fetch' }); @@ -230,12 +242,12 @@ export class CommandManager extends Disposable { showErrorMessage('An unexpected error occurred while running the command "Fetch from Remote(s)".'); }); } else if (repoPaths.length === 1) { - GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.repoManager, this.logger, { + GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.cicdManager, this.repoManager, this.logger, { repo: repoPaths[0], runCommandOnLoad: 'fetch' }); } else { - GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.repoManager, this.logger, null); + GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.cicdManager, this.repoManager, this.logger, null); } } @@ -291,7 +303,7 @@ export class CommandManager extends Disposable { }).then((item) => { if (item) { const commitHashes = item.codeReviewId.split('-'); - GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.repoManager, this.logger, { + GitGraphView.createOrShow(this.context.extensionPath, this.dataSource, this.extensionState, this.avatarManager, this.cicdManager, this.repoManager, this.logger, { repo: item.codeReviewRepo, commitDetails: { commitHash: commitHashes[commitHashes.length > 1 ? 1 : 0], diff --git a/src/config.ts b/src/config.ts index 88a05740..343e413d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -356,6 +356,13 @@ class Config { return !!this.getRenamedExtensionSetting('repository.commits.fetchAvatars', 'fetchAvatars', false); } + /** + * Get the value of the `git-graph.repository.commits.fetchCICDsMaximumStatuses` Extension Setting. + */ + get fetchCICDsMaximumStatuses() { + return this.getRenamedExtensionSetting('repository.commits.fetchCICDsMaximumStatuses', 'fetchCICDsMaximumStatuses', 1000); + } + /** * Get the value of the `git-graph.repository.commits.initialLoad` Extension Setting. */ diff --git a/src/extension.ts b/src/extension.ts index 946b87cd..ba1d179e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { AvatarManager } from './avatarManager'; +import { CicdManager } from './cicdManager'; import { CommandManager } from './commands'; import { getConfig } from './config'; import { DataSource } from './dataSource'; @@ -42,8 +43,9 @@ export async function activate(context: vscode.ExtensionContext) { const dataSource = new DataSource(gitExecutable, onDidChangeConfiguration, onDidChangeGitExecutable, logger); const avatarManager = new AvatarManager(dataSource, extensionState, logger); const repoManager = new RepoManager(dataSource, extensionState, onDidChangeConfiguration, logger); + const cicdManager = new CicdManager(extensionState, repoManager, logger); const statusBarItem = new StatusBarItem(repoManager.getNumRepos(), repoManager.onDidChangeRepos, onDidChangeConfiguration, logger); - const commandManager = new CommandManager(context, avatarManager, dataSource, extensionState, repoManager, gitExecutable, onDidChangeGitExecutable, logger); + const commandManager = new CommandManager(context, avatarManager, cicdManager, dataSource, extensionState, repoManager, gitExecutable, onDidChangeGitExecutable, logger); const diffDocProvider = new DiffDocProvider(dataSource); context.subscriptions.push( @@ -73,6 +75,7 @@ export async function activate(context: vscode.ExtensionContext) { statusBarItem, repoManager, avatarManager, + cicdManager, dataSource, configurationEmitter, extensionState, diff --git a/src/extensionState.ts b/src/extensionState.ts index 286ab1ba..993385ad 100644 --- a/src/extensionState.ts +++ b/src/extensionState.ts @@ -1,14 +1,17 @@ +import * as crypto from 'crypto'; import * as fs from 'fs'; import * as vscode from 'vscode'; import { Avatar, AvatarCache } from './avatarManager'; +import { CICDCache } from './cicdManager'; import { getConfig } from './config'; -import { BooleanOverride, CodeReview, ErrorInfo, FileViewType, GitGraphViewGlobalState, GitGraphViewWorkspaceState, GitRepoSet, GitRepoState, RepoCommitOrdering } from './types'; -import { GitExecutable, getPathFromStr } from './utils'; +import { BooleanOverride, CICDConfig, CICDDataSave, CodeReview, ErrorInfo, FileViewType, GitGraphViewGlobalState, GitGraphViewWorkspaceState, GitRepoSet, GitRepoState, RepoCommitOrdering } from './types'; +import { GitExecutable, getNonce, getPathFromStr } from './utils'; import { Disposable } from './utils/disposable'; import { Event } from './utils/event'; const AVATAR_STORAGE_FOLDER = '/avatars'; const AVATAR_CACHE = 'avatarCache'; +const CICD_CACHE = 'cicdCache'; const CODE_REVIEWS = 'codeReviews'; const GLOBAL_VIEW_STATE = 'globalViewState'; const IGNORED_REPOS = 'ignoredRepos'; @@ -32,6 +35,8 @@ export const DEFAULT_REPO_STATE: GitRepoState = { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, @@ -120,6 +125,34 @@ export class ExtensionState extends Disposable { outputSet[repo].showRemoteBranchesV2 = repoSet[repo].showRemoteBranches ? BooleanOverride.Enabled : BooleanOverride.Disabled; } } + // Decrypt cicdToken + const cicdConfigs = repoSet[repo].cicdConfigs; + if (typeof repoSet[repo].cicdConfigs !== 'undefined' && cicdConfigs !== null) { + outputSet[repo].cicdConfigs = []; + const cicdConfigsOut: CICDConfig[] = []; + if (typeof repoSet[repo].cicdNonce !== 'undefined' && repoSet[repo].cicdNonce !== null) { + let ENCRYPTION_KEY = repoSet[repo].cicdNonce; // Must be 256 bits (32 characters) + cicdConfigs.forEach(element => { + if (typeof element.cicdUrl !== 'undefined' && typeof element.cicdToken !== 'undefined') { + let textParts: string[] = element.cicdToken.split(':'); + let iv = Buffer.from(textParts[0], 'hex'); + let encryptedText = Buffer.from(textParts[1], 'hex'); + + let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); + let decrypted = decipher.update(encryptedText); + + decrypted = Buffer.concat([decrypted, decipher.final()]); + let config: CICDConfig = { + provider: element.provider, + cicdUrl: element.cicdUrl, + cicdToken: decrypted.toString() + }; + cicdConfigsOut.push(config); + } + }); + } + outputSet[repo].cicdConfigs = cicdConfigsOut; + } }); return outputSet; } @@ -129,7 +162,36 @@ export class ExtensionState extends Disposable { * @param gitRepoSet The set of repositories. */ public saveRepos(gitRepoSet: GitRepoSet) { - this.updateWorkspaceState(REPO_STATES, gitRepoSet); + // Deep Clone gitRepoSet + let gitRepoSetTemp = JSON.parse(JSON.stringify(gitRepoSet)); + + // Encrypt cicdToken + Object.keys(gitRepoSetTemp).forEach((repo) => { + let ENCRYPTION_KEY = getNonce(); // Must be 256 bits (32 characters) + const IV_LENGTH = 16; // For AES, this is always 16 + gitRepoSetTemp[repo].cicdNonce = ENCRYPTION_KEY; + + // Create Encrypted cicdToken + let cicdConfigsEncrypto: CICDConfig[] = []; + gitRepoSetTemp[repo].cicdConfigs?.forEach((cicdConfig: CICDConfig) => { + let iv = crypto.randomBytes(IV_LENGTH); + let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); + let plain = Buffer.from(cicdConfig.cicdToken); + let encrypted = Buffer.concat([ + cipher.update(plain), + cipher.final() + ]); + let config: CICDConfig = { + provider: cicdConfig.provider, + cicdUrl: cicdConfig.cicdUrl, + cicdToken: iv.toString('hex') + ':' + encrypted.toString('hex') + }; + cicdConfigsEncrypto.push(config); + }); + // Replace Encrypted cicdToken + gitRepoSetTemp[repo].cicdConfigs = cicdConfigsEncrypto; + }); + this.updateWorkspaceState(REPO_STATES, gitRepoSetTemp); } /** @@ -314,6 +376,43 @@ export class ExtensionState extends Disposable { } + /* CICDs */ + + /** + * Gets the cache of cicds known to Git Graph. + * @returns The cicd cache. + */ + public getCICDCache() { + return this.workspaceState.get(CICD_CACHE, {}); + } + + /** + * Add a new cicd to the cache of cicds known to Git Graph. + * @param repo The repository that the cicd is used in. + * @param hash The hash identifying the cicd commit. + * @param id The identifying that the cicd job. + * @param cicdDataSave The CICDDataSave. + */ + public saveCICD(repo: string, hash: string, id: string, cicdDataSave: CICDDataSave) { + let cicds = this.getCICDCache(); + if (typeof cicds[repo] === 'undefined') { + cicds[repo] = {}; + } + if (typeof cicds[repo][hash] === 'undefined') { + cicds[repo][hash] = {}; + } + cicds[repo][hash][id] = cicdDataSave; + this.updateWorkspaceState(CICD_CACHE, cicds); + } + + /** + * Clear all cicds from the cache of cicds known to Git Graph. + */ + public clearCICDCache() { + this.updateWorkspaceState(CICD_CACHE, {}); + } + + /* Code Review */ // Note: id => the commit arguments to 'git diff' (either or -) diff --git a/src/gitGraphView.ts b/src/gitGraphView.ts index ed3f8722..31b619c3 100644 --- a/src/gitGraphView.ts +++ b/src/gitGraphView.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { AvatarManager } from './avatarManager'; +import { CicdManager } from './cicdManager'; import { getConfig } from './config'; import { DataSource, GitCommitDetailsData, GitConfigKey } from './dataSource'; import { ExtensionState } from './extensionState'; @@ -20,6 +21,7 @@ export class GitGraphView extends Disposable { private readonly panel: vscode.WebviewPanel; private readonly extensionPath: string; private readonly avatarManager: AvatarManager; + private readonly cicdManager: CicdManager; private readonly dataSource: DataSource; private readonly extensionState: ExtensionState; private readonly repoFileWatcher: RepoFileWatcher; @@ -39,11 +41,12 @@ export class GitGraphView extends Disposable { * @param dataSource The Git Graph DataSource instance. * @param extensionState The Git Graph ExtensionState instance. * @param avatarManger The Git Graph AvatarManager instance. + * @param cicdManager The Git Graph CicdManager instance. * @param repoManager The Git Graph RepoManager instance. * @param logger The Git Graph Logger instance. * @param loadViewTo What to load the view to. */ - public static createOrShow(extensionPath: string, dataSource: DataSource, extensionState: ExtensionState, avatarManager: AvatarManager, repoManager: RepoManager, logger: Logger, loadViewTo: LoadGitGraphViewTo) { + public static createOrShow(extensionPath: string, dataSource: DataSource, extensionState: ExtensionState, avatarManager: AvatarManager, cicdManager: CicdManager, repoManager: RepoManager, logger: Logger, loadViewTo: LoadGitGraphViewTo) { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; if (GitGraphView.currentPanel) { @@ -60,7 +63,7 @@ export class GitGraphView extends Disposable { GitGraphView.currentPanel.panel.reveal(column); } else { // If Git Graph panel doesn't already exist - GitGraphView.currentPanel = new GitGraphView(extensionPath, dataSource, extensionState, avatarManager, repoManager, logger, loadViewTo, column); + GitGraphView.currentPanel = new GitGraphView(extensionPath, dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, loadViewTo, column); } } @@ -70,15 +73,17 @@ export class GitGraphView extends Disposable { * @param dataSource The Git Graph DataSource instance. * @param extensionState The Git Graph ExtensionState instance. * @param avatarManger The Git Graph AvatarManager instance. + * @param cicdManager The Git Graph CicdManager instance. * @param repoManager The Git Graph RepoManager instance. * @param logger The Git Graph Logger instance. * @param loadViewTo What to load the view to. * @param column The column the view should be loaded in. */ - private constructor(extensionPath: string, dataSource: DataSource, extensionState: ExtensionState, avatarManager: AvatarManager, repoManager: RepoManager, logger: Logger, loadViewTo: LoadGitGraphViewTo, column: vscode.ViewColumn | undefined) { + private constructor(extensionPath: string, dataSource: DataSource, extensionState: ExtensionState, avatarManager: AvatarManager, cicdManager: CicdManager, repoManager: RepoManager, logger: Logger, loadViewTo: LoadGitGraphViewTo, column: vscode.ViewColumn | undefined) { super(); this.extensionPath = extensionPath; this.avatarManager = avatarManager; + this.cicdManager = cicdManager; this.dataSource = dataSource; this.extensionState = extensionState; this.repoManager = repoManager; @@ -143,6 +148,16 @@ export class GitGraphView extends Disposable { }); }), + // Subscribe to events triggered when an cicd is available + cicdManager.onCICD((event) => { + this.sendMessage({ + command: 'fetchCICD', + repo: event.repo, + hash: event.hash, + cicdDataSaves: event.cicdDataSaves + }); + }), + // Respond to messages sent from the Webview this.panel.webview.onDidReceiveMessage((msg) => this.respondToMessage(msg)), @@ -228,20 +243,22 @@ export class GitGraphView extends Disposable { }); break; case 'commitDetails': - let data = await Promise.all([ + let data = await Promise.all([ msg.commitHash === UNCOMMITTED ? this.dataSource.getUncommittedDetails(msg.repo) : msg.stash === null ? this.dataSource.getCommitDetails(msg.repo, msg.commitHash, msg.hasParents) : this.dataSource.getStashDetails(msg.repo, msg.commitHash, msg.stash), - msg.avatarEmail !== null ? this.avatarManager.getAvatarImage(msg.avatarEmail) : Promise.resolve(null) + msg.avatarEmail !== null ? this.avatarManager.getAvatarImage(msg.avatarEmail) : Promise.resolve(null), + this.cicdManager.getCICDDetail(msg.repo, msg.commitHash) ]); this.sendMessage({ command: 'commitDetails', ...data[0], avatar: data[1], codeReview: msg.commitHash !== UNCOMMITTED ? this.extensionState.getCodeReview(msg.repo, msg.commitHash) : null, - refresh: msg.refresh + refresh: msg.refresh, + cicdDataSaves: JSON.parse(data[2] || '{}') }); break; case 'compareCommits': @@ -661,6 +678,7 @@ export class GitGraphView extends Disposable { fetchAndPrune: config.fetchAndPrune, fetchAndPruneTags: config.fetchAndPruneTags, fetchAvatars: config.fetchAvatars && this.extensionState.isAvatarStorageAvailable(), + fetchCICDsMaximumStatuses: config.fetchCICDsMaximumStatuses, graph: config.graph, includeCommitsMentionedByReflogs: config.includeCommitsMentionedByReflogs, initialLoadCommits: config.initialLoadCommits, diff --git a/src/types.ts b/src/types.ts index 8410deae..4aaf03a2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -197,6 +197,49 @@ interface PullRequestConfigCustom extends PullRequestConfigBase { export type PullRequestConfig = PullRequestConfigBuiltIn | PullRequestConfigCustom; +export interface CICDData { + id: string; + status: string; + ref: string; + sha: string; + web_url: string; + created_at: string; + updated_at: string; + name: string; + event: string; + detail: boolean; +} + +export interface CICDDataSave { + name: string; + status: string; + ref: string; + web_url: string; + event: string; + detail: boolean; + allow_failure: boolean; +} + +export interface CICDConfigBase { + readonly cicdUrl: string; + readonly cicdToken: string; +} + +export const enum CICDProvider { + Bitbucket, + Custom, + GitHubV3, + GitLabV4, + JenkinsV2 +} + +interface CICDConfigBuiltIn extends CICDConfigBase { + readonly provider: CICDProvider; +} + + +export type CICDConfig = CICDConfigBuiltIn; + export interface GitRepoState { cdvDivider: number; cdvHeight: number; @@ -212,6 +255,8 @@ export interface GitRepoState { onRepoLoadShowCheckedOutBranch: BooleanOverride; onRepoLoadShowSpecificBranches: string[] | null; pullRequestConfig: PullRequestConfig | null; + cicdConfigs: CICDConfig[] | null; + cicdNonce: string | null; showRemoteBranches: boolean; showRemoteBranchesV2: BooleanOverride; showStashes: BooleanOverride; @@ -245,6 +290,7 @@ export interface GitGraphViewConfig { readonly fetchAndPrune: boolean; readonly fetchAndPruneTags: boolean; readonly fetchAvatars: boolean; + readonly fetchCICDsMaximumStatuses: number; readonly graph: GraphConfig; readonly includeCommitsMentionedByReflogs: boolean; readonly initialLoadCommits: number; @@ -672,6 +718,7 @@ export interface ResponseCommitDetails extends ResponseWithErrorInfo { readonly avatar: string | null; readonly codeReview: CodeReview | null; readonly refresh: boolean; + readonly cicdDataSaves: { [id: string]: CICDDataSave }; } export interface RequestCompareCommits extends RepoRequest { @@ -866,6 +913,17 @@ export interface ResponseFetchAvatar extends BaseMessage { readonly email: string; readonly image: string; } +export interface RequestFetchCICD extends RepoRequest { + readonly command: 'fetchCICD'; + readonly repo: string; + readonly hash: string; +} +export interface ResponseFetchCICD extends BaseMessage { + readonly command: 'fetchCICD'; + readonly repo: string; + readonly hash: string; + readonly cicdDataSaves: { [id: string]: CICDDataSave }; +} export interface RequestFetchIntoLocalBranch extends RepoRequest { readonly command: 'fetchIntoLocalBranch'; @@ -1241,6 +1299,7 @@ export type RequestMessage = | RequestExportRepoConfig | RequestFetch | RequestFetchAvatar + | RequestFetchCICD | RequestFetchIntoLocalBranch | RequestLoadCommits | RequestLoadConfig @@ -1303,6 +1362,7 @@ export type ResponseMessage = | ResponseExportRepoConfig | ResponseFetch | ResponseFetchAvatar + | ResponseFetchCICD | ResponseFetchIntoLocalBranch | ResponseLoadCommits | ResponseLoadConfig diff --git a/tests/cicdManager.test.ts b/tests/cicdManager.test.ts new file mode 100644 index 00000000..92f623f9 --- /dev/null +++ b/tests/cicdManager.test.ts @@ -0,0 +1,3790 @@ +import * as date from './mocks/date'; +import * as vscode from './mocks/vscode'; +jest.mock('vscode', () => vscode, { virtual: true }); +jest.mock('fs'); +jest.mock('https'); +jest.mock('http'); +jest.mock('../src/dataSource'); +jest.mock('../src/extensionState'); +jest.mock('../src/logger'); +jest.mock('../src/repoManager'); + +import { ClientRequest, IncomingMessage } from 'http'; +import * as https from 'https'; +import * as http from 'http'; +import { URL } from 'url'; +import { ConfigurationChangeEvent } from 'vscode'; +import { CICDEvent, CicdManager } from '../src/cicdManager'; +import { DataSource } from '../src/dataSource'; +import { DEFAULT_REPO_STATE, ExtensionState } from '../src/extensionState'; +import { Logger } from '../src/logger'; +import { GitExecutable } from '../src/utils'; +import { EventEmitter } from '../src/utils/event'; +import { RepoManager } from '../src/repoManager'; +import { CICDConfig, CICDProvider } from '../src/types'; +import { waitForExpect } from './helpers/expectations'; + +let onDidChangeConfiguration: EventEmitter; +let onDidChangeGitExecutable: EventEmitter; +let logger: Logger; +let dataSource: DataSource; +let extensionState: ExtensionState; +let repoManager: RepoManager; +let spyOnSaveCicd: jest.SpyInstance, spyOnHttpsGet: jest.SpyInstance, spyOnHttpGet: jest.SpyInstance, spyOnLog: jest.SpyInstance; +let spyOnGetRepos: jest.SpyInstance; +// , spyOnGetKnownRepo: jest.SpyInstance, spyOnRegisterRepo: jest.SpyInstance, spyOnGetCommitSubject: jest.SpyInstance; +// let spyOnGetCodeReviews: jest.SpyInstance; +// , spyOnEndCodeReview: jest.SpyInstance; +const GitHubResponse = JSON.stringify({ + total_count: 1, + check_runs: [{ + id: 2211653232, + head_sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + html_url: 'https://github.com/mhutchie/vscode-git-graph/runs/2211653232', + status: 'completed', + conclusion: 'success', + name: 'build', + app: { name: 'GitHub Actions' } + }] +}); + +const GitLabResponse = JSON.stringify([ + { + id: 2211653232, + sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + ref: 'main', + status: 'success', + name: 'eslint-sast', + target_url: 'https://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + allow_failure: false + } +]); + +const JenkinsResponse = JSON.stringify( + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowJob', + builds: [ + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowRun', + actions: [ + { + _class: 'hudson.plugins.git.util.BuildData', + lastBuiltRevision: { + branch: [ + { + SHA1: '149ecc50e5c223251f80a0223cfbbd9822307224', + name: 'master' + } + ] + } + } + ], + fullDisplayName: 'job01 » MultiBranch » master #3', + id: '3', + result: 'SUCCESS', + timestamp: 1620716982997, + url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/' + } + ] + } +); + +const GitHubHeader = { + 'content-type': 'application/json; charset=utf-8', + 'link': '; rel="next", ; rel="last"', + 'x-github-media-type': 'unknown, github.v3', + 'x-ratelimit-limit': '60', + 'x-ratelimit-remaining': '57', + 'x-ratelimit-reset': '1618343683' +}; + +const GitLabHeader = { + 'content-type': 'application/json', + 'x-page': '1', + 'x-total': '32500', + 'x-total-pages': '325', + 'ratelimit-limit': '60', + 'ratelimit-observed': '3', + 'ratelimit-remaining': '57', + 'ratelimit-reset': '1618343683' +}; + +const JenkinsHeader = { + 'content-type': 'application/json', + 'x-jenkins': '2.235.1' +}; + +const GitHubCicdEvents = { + 'cicdDataSaves': { + '2211653232': { + name: 'GitHub Actions(build)', + status: 'success', + ref: '', + web_url: 'https://github.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' +}; + +const GitLabCicdEvents = { + 'cicdDataSaves': { + '2211653232': { + name: 'eslint-sast', + status: 'success', + ref: 'main', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' +}; + + +const JenkinsCicdEvents = { + 'cicdDataSaves': { + '3': { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' +}; + +const GitHubHttpsGet = { + hostname: 'api.github.com', + path: '/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 +}; + +const GitLabHttpsGet = { + hostname: 'gitlab.com', + path: '/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100', + headers: { + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 +}; + +const JenkinsHttpsGet = { + hostname: 'jenkins.net', + path: '/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]', + headers: { + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 +}; + +const GitHubGetRspos = { + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) +}; + +const GitLabGetRspos = { + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) +}; + +const JenkinsGetRspos = { + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) +}; + +const GitHubSaveCicd = [ + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', 2211653232, + { + name: 'GitHub Actions(build)', + status: 'success', + ref: '', + web_url: 'https://github.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } +]; + +const GitLabSaveCicd = [ + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', 2211653232, + { + name: 'eslint-sast', + status: 'success', + ref: 'main', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } +]; + +const JenkinsSaveCicd = [ + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', '3', + { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + } +]; + +beforeAll(() => { + onDidChangeConfiguration = new EventEmitter(); + onDidChangeGitExecutable = new EventEmitter(); + logger = new Logger(); + dataSource = new DataSource(null, onDidChangeConfiguration.subscribe, onDidChangeGitExecutable.subscribe, logger); + extensionState = new ExtensionState(vscode.mocks.extensionContext, onDidChangeGitExecutable.subscribe); + repoManager = new RepoManager(dataSource, extensionState, onDidChangeConfiguration.subscribe, logger); + spyOnGetRepos = jest.spyOn(repoManager, 'getRepos'); + // spyOnGetKnownRepo = jest.spyOn(repoManager, 'getKnownRepo'); + // spyOnRegisterRepo = jest.spyOn(repoManager, 'registerRepo'); + spyOnSaveCicd = jest.spyOn(extensionState, 'saveCICD'); + // spyOnGetCodeReviews = jest.spyOn(extensionState, 'getCodeReviews'); + // spyOnEndCodeReview = jest.spyOn(extensionState, 'endCodeReview'); + // spyOnGetCommitSubject = jest.spyOn(dataSource, 'getCommitSubject'); + spyOnHttpsGet = jest.spyOn(https, 'get'); + spyOnHttpGet = jest.spyOn(http, 'get'); + spyOnLog = jest.spyOn(logger, 'log'); +}); + +afterAll(() => { + extensionState.dispose(); + repoManager.dispose(); + dataSource.dispose(); + logger.dispose(); + onDidChangeConfiguration.dispose(); + onDidChangeGitExecutable.dispose(); +}); + + +describe('CicdManager', () => { + let cicdManager: CicdManager; + // let spyOnGetLastActiveRepo: jest.SpyInstance; + // beforeAll(() => { + // spyOnGetLastActiveRepo = jest.spyOn(extensionState, 'getLastActiveRepo'); + // }); + beforeEach(() => { + jest.spyOn(extensionState, 'getCICDCache').mockReturnValueOnce({ + '/path/to/repo': { + 'hash0': { + 'id0': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: false + } + } + } + }); + cicdManager = new CicdManager(extensionState, repoManager, logger); + jest.clearAllTimers(); + jest.useRealTimers(); + }); + afterEach(() => { + cicdManager.dispose(); + }); + + it('Should construct an CicdManager, and be disposed', () => { + // Assert + expect(cicdManager['disposables']).toHaveLength(2); + + // Run + cicdManager.dispose(); + + // Assert + expect(cicdManager['disposables']).toHaveLength(0); + }); + + describe('getCICDDetail', () => { + it('Should trigger the cicd to not be emitted when a known cicd is requested', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo': mockRepoState(null, 0, null, null) + }); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo', 'hash0'); + + // Assert + expect.assertions(1); + if (data) { + expect(JSON.parse(data)).toStrictEqual({ + 'id0': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: false + }}); + } + }); + + it('Should trigger the cicd to not be emitted when a unknown cicd is requested multi repo', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, null, null), + '/path/to/repo2': mockRepoState(null, 0, null, null) + }); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo2', 'hash0'); + + // Assert + expect(data).toStrictEqual(null); + }); + + it('Should trigger the cicd to not be emitted when a unknown cicd is requested', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, null, null) + }); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repoX', 'hash0'); + + // Assert + expect(data).toStrictEqual(null); + }); + + + describe('Unknown provider', () => { + it('Should Unknown provider Error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: -1, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenCalledWith( 'Unknown provider Error'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + }); + + + describe('GitHub', () => { + it('Should fetch a new cicd from GitHub (HTTPS Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(200, GitHubResponse, GitHubHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitHubCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitHubSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitHub (HTTPS Remote) with port', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com:80/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpsResponse(200, GitHubResponse, GitHubHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitHubCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'api.github.com', + path: '/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'vscode-git-graph' + }, + port: '80', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitHubSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://github.com:80/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitHub (HTTP Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'http://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpResponse(200, JSON.stringify({ + total_count: 1, + check_runs: [{ + id: 2211653232, + head_sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + html_url: 'http://github.com/mhutchie/vscode-git-graph/runs/2211653232', + status: 'completed', + conclusion: 'success', + name: 'build', + app: { name: 'GitHub Actions' } + }] + }), GitHubHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '2211653232': { + name: 'GitHub Actions(build)', + status: 'success', + ref: '', + web_url: 'http://github.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', 2211653232, + { + name: 'GitHub Actions(build)', + status: 'success', + ref: '', + web_url: 'http://github.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for http://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)http://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for http://github.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitHub (SSH Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'git@github.com:keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (git@github.com:keydepth/vscode-git-graph.git) for GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new cicd from GitHub (HTTPS Remote) not detail', async () => { + // Setup + jest.useFakeTimers(); + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(200, JSON.stringify({ + total_count: 324, + workflow_runs: [ + { + id: 740791415, + name: 'Update Milestone on Release', + head_branch: 'v1.31.0-beta.0', + head_sha: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + event: 'release', + status: 'completed', + conclusion: 'success', + url: 'https://api.github.com/repos/mhutchie/vscode-git-graph/actions/runs/740791415', + html_url: 'https://github.com/mhutchie/vscode-git-graph/actions/runs/740791415', + pull_requests: [], + created_at: '2021-04-12T10:24:48Z', + updated_at: '2021-04-12T10:25:06Z' + } + ] + }), Object.assign({}, GitHubHeader, { + 'link': '; rel="next", ; rel="last"' + })); + mockHttpsResponse(200, GitHubResponse, Object.assign({}, GitHubHeader, { + 'link': '; rel="next", ; rel="last"' + })); + const cicdEvents = waitForEvents(cicdManager, 2, true); + cicdManager['queue']['queue'] = [{ + repo: '/path/to/repo1', + cicdConfig: { + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: false, + hash: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + maximumStatuses: 1000 + }]; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357'); + cicdManager['queue']['itemsAvailableCallback'](); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '740791415': { + name: 'Update Milestone on Release', + status: 'success', + ref: 'v1.31.0-beta.0', + web_url: 'https://github.com/mhutchie/vscode-git-graph/actions/runs/740791415', + event: 'release', + detail: false, + allow_failure: false + } + }, + 'hash': 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + 'repo': '/path/to/repo1' + }, GitHubCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'api.github.com', + path: '/repos/keydepth/vscode-git-graph/actions/runs?per_page=100', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357', 740791415, + { + name: 'Update Milestone on Release', + status: 'success', + ref: 'v1.31.0-beta.0', + web_url: 'https://github.com/mhutchie/vscode-git-graph/actions/runs/740791415', + event: 'release', + detail: false, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/actions/runs?per_page=100 detail=false page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenCalledWith('GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/actions/runs?per_page=100'); + expect(spyOnLog).toHaveBeenCalledWith('Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/b9112e60f5fb3d8bc2a387840577b4756a12f357/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenCalledWith('GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/b9112e60f5fb3d8bc2a387840577b4756a12f357/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenCalledWith('Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(6); + jest.useRealTimers(); + }); + + it('Should fetch a new cicd from GitHub (HTTPS Remote) not detail with no conclusion', async () => { + // Setup + jest.useFakeTimers(); + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(200, JSON.stringify({ + total_count: 324, + workflow_runs: [ + { + id: 740791415, + name: 'Update Milestone on Release', + head_branch: 'v1.31.0-beta.0', + head_sha: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + event: 'release', + status: 'pending', + conclusion: null, + url: 'https://api.github.com/repos/mhutchie/vscode-git-graph/actions/runs/740791415', + html_url: 'https://github.com/mhutchie/vscode-git-graph/actions/runs/740791415', + pull_requests: [], + created_at: '2021-04-12T10:24:48Z', + updated_at: '2021-04-12T10:25:06Z' + } + ] + }), Object.assign({}, GitHubHeader, { + 'link': '; rel="next", ; rel="last"' + })); + mockHttpsResponse(200, JSON.stringify({ + total_count: 1, + check_runs: [{ + id: 2211653232, + head_sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + html_url: 'https://github.com/mhutchie/vscode-git-graph/runs/2211653232', + status: 'pending', + conclusion: null, + name: 'build', + app: { name: 'GitHub Actions' } + }] + }), Object.assign({}, GitHubHeader, { + 'link': '; rel="next", ; rel="last"' + })); + const cicdEvents = waitForEvents(cicdManager, 2, true); + cicdManager['queue']['queue'] = [{ + repo: '/path/to/repo1', + cicdConfig: { + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: false, + hash: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + maximumStatuses: 1000 + }]; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357'); + cicdManager['queue']['itemsAvailableCallback'](); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '740791415': { + name: 'Update Milestone on Release', + status: 'pending', + ref: 'v1.31.0-beta.0', + web_url: 'https://github.com/mhutchie/vscode-git-graph/actions/runs/740791415', + event: 'release', + detail: false, + allow_failure: false + } + }, + 'hash': 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + 'repo': '/path/to/repo1' + }, { + 'cicdDataSaves': { + '2211653232': { + name: 'GitHub Actions(build)', + status: 'pending', + ref: '', + web_url: 'https://github.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'api.github.com', + path: '/repos/keydepth/vscode-git-graph/actions/runs?per_page=100', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357', 740791415, + { + name: 'Update Milestone on Release', + status: 'pending', + ref: 'v1.31.0-beta.0', + web_url: 'https://github.com/mhutchie/vscode-git-graph/actions/runs/740791415', + event: 'release', + detail: false, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/actions/runs?per_page=100 detail=false page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenCalledWith('GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/actions/runs?per_page=100'); + expect(spyOnLog).toHaveBeenCalledWith('Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/b9112e60f5fb3d8bc2a387840577b4756a12f357/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenCalledWith('GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/b9112e60f5fb3d8bc2a387840577b4756a12f357/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenCalledWith('Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(6); + jest.useRealTimers(); + }); + + it('Should fetch a new cicd from GitHub (HTTPS Remote) with ratelimit header', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(200, GitHubResponse, Object.assign({}, GitHubHeader, { + 'link': '; rel="next", ; rel"last", ; rel=last', + 'x-ratelimit-limit': '', + 'x-ratelimit-remaining': '', + 'x-ratelimit-reset': '' + })); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitHubCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitHubSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=None(1 hour)/Remaining=None) from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should fetch a new cicd from GitHub (HTTPS Remote) with link error header (number)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(200, GitHubResponse, Object.assign({}, GitHubHeader, { + 'link': 0 + })); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitHubCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitHubSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should fetch a new cicd from GitHub (HTTPS Remote) with link error header (string)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(200, GitHubResponse, Object.assign({}, GitHubHeader, { + 'link': '' + })); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitHubCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitHubSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should fetch a new cicd from GitHub (HTTPS Remote) with link error header (page)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(200, GitHubResponse, Object.assign({}, GitHubHeader, { + 'link': '; rel="next", ; rel="last"' + })); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitHubCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitHubSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should fetch a new cicd from GitHub (HTTPS Remote) no emmit', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(200, GitHubResponse, GitHubHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitHub (URL is Not Match)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'ftp://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (ftp://github.com/keydepth/vscode-git-graph.git) for GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new cicd from GitHub (Bad URL)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (github.com/keydepth/vscode-git-graph.git) for GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new multiple cicd from GitHub', async () => { + // Setup + jest.useFakeTimers(); + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + for (let i = 0; i < 10; i++) { + mockHttpsResponse(200, GitHubResponse, GitHubHeader); + } + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitHubCicdEvents]); + expect(cicdManager['queue']['queue'].length).toBe(9); + expect(cicdManager['queue']['queue'][0].attempts).toBe(0); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + jest.runAllTimers(); + for (let i = 1; i < 10; i++) { + // jest.runOnlyPendingTimers(); + expect(spyOnLog).toHaveBeenNthCalledWith(3 + i * 2, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100&page=' + (i + 1) + ' detail=true page=' + (i + 1) + ' from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(4 + i * 2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100&page=' + (i + 1)); + } + jest.useRealTimers(); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitHubSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://github.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(1 hour)/Remaining=57/' + new Date(1618343683).toString() + ') from GitHub'); + expect(spyOnLog).toHaveBeenCalledTimes(22); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo', async () => { + // Setup + const cicdEvents = waitForEvents(cicdManager, 1); + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, GitHubResponse, GitHubHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo', 'hash0'); + + // Assert + expect.assertions(2); + if (data) { + expect(JSON.parse(data)).toStrictEqual({ + 'id0': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: false + } + }); + } + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '2211653232': { + name: 'GitHub Actions(build)', + status: 'success', + ref: '', + web_url: 'https://github.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo' + }]); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with cicdToken', async () => { + // Setup + const cicdEvents = waitForEvents(cicdManager, 1); + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, 'nonce', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: 'aaaaaa' + }]) + }); + mockHttpsResponse(200, GitHubResponse, GitHubHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash0'); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '2211653232': { + name: 'GitHub Actions(build)', + status: 'success', + ref: '', + web_url: 'https://github.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'api.github.com', + path: '/repos/keydepth/vscode-git-graph/commits/hash0/check-runs?per_page=100', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'vscode-git-graph', + 'Authorization': 'token aaaaaa' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with responce is empty JSON', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, JSON.stringify({}), GitHubHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo', 'hash0'); + + // Assert + expect.assertions(1); + if (data) { + expect(JSON.parse(data)).toStrictEqual({ + 'id0': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: false + } + }); + } + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with responce is Not JSON format', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, 'Not JSON format', GitHubHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect.assertions(5); + expect(data).toStrictEqual(null); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API - (200)https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'GitHub API Error - (200)API Result error. : Unexpected token N in JSON at position 0'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should halt fetching the cicd when the GitHub cicd url request is unsuccessful', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(404, '', GitHubHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API Error - (404)undefined'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should halt fetching the cicd when the GitHub cicd url request is unsuccessful with Message Body', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(404, '{"message":"Error Message Body"}', GitHubHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API Error - (404)Error Message Body'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should halt fetching the cicd when the GitHub cicd url request is unsuccessful with No Message Body', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(404, '{}', GitHubHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API Error - (404)undefined'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should requeue the request when the GitHub API cannot find the commit', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(422, '', Object.assign({}, GitHubHeader, { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': (date.now + 1).toString() })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['queue']['queue'].length).toBe(1); + expect(cicdManager['queue']['queue'][0].attempts).toBe(1); + }); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: 0, + attempts: 1, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitHubHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API Rate Limit Reached - Paused fetching from GitHub until the Rate Limit is reset (RateLimit=60(1 hour)/' + new Date(1618343683).toString() + ')'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'GitHub API Rate Limit can upgrade by Access Token.'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should set the GitHub API timeout and requeue the request when the rate limit is reached with cicdToken', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: 'aaaaaa' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpsResponse(403, '', Object.assign({}, GitHubHeader, { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': (date.now + 1).toString() })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('GitHub API Rate Limit Reached - Paused fetching from GitHub until the Rate Limit is reset (RateLimit=60(1 hour)/' + new Date(1618343683).toString() + ')'); + }); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: 'aaaaaa', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: (date.now + 1) * 1000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'api.github.com', + path: '/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'vscode-git-graph', + 'Authorization': 'token aaaaaa' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://api.github.com/repos/keydepth/vscode-git-graph/commits/149ecc50e5c223251f80a0223cfbbd9822307224/check-runs?per_page=100 detail=true page=-1 from GitHub'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitHub API Rate Limit Reached - Paused fetching from GitHub until the Rate Limit is reset (RateLimit=60(1 hour)/' + new Date(1618343683).toString() + ')'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + expect(cicdManager['gitHubTimeout']).toBe((date.now + 1) * 1000); + }); + + it('Should set the GitHub API timeout and requeue the request when the API returns a 5xx error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsResponse(500, '', GitHubHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitHubTimeout']).toBe(date.now * 1000 + 600000); + }); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: date.now * 1000 + 600000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the GitHub API timeout and requeue the request when there is an HTTPS Client Request Error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsClientRequestErrorEvent({ message: 'Error Message' }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitHubTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('GitHub API https Error - Error Message'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the GitHub API timeout and requeue the request when there is an HTTPS Client Request Error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsClientRequestErrorEvent({ message: 'Error Message' }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitHubTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('GitHub API https Error - Error Message'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the GitHub API timeout and requeue the request when there is an HTTPS Incoming Message Error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsIncomingMessageErrorEvent(); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitHubTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('GitHub API https Error - undefined'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the GitHub API timeout and requeue the request once when there are multiple HTTPS Error Events', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + mockHttpsMultipleErrorEvents(); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitHubTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('GitHub API https Error - undefined'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should requeue the request when it\'s before the GitHub API timeout', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitHubGetRspos); + cicdManager['gitHubTimeout'] = (date.now + 1) * 1000; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: (date.now + 1) * 1000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + }); + + it('Should insert requests into the priority queue in the correct order', async () => { + // Setup + spyOnGetRepos.mockReturnValue({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + mockHttpsResponse(403, '', Object.assign({}, GitHubHeader, { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': (date.now + 1).toString() })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash0'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('GitHub API Rate Limit Reached - Paused fetching from GitHub until the Rate Limit is reset (RateLimit=60(1 hour)/' + new Date(1618343683).toString() + ')'); + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash1'); + await cicdManager.getCICDDetail('/path/to/repo1', 'hash2'); + await cicdManager.getCICDDetail('/path/to/repo1', 'hash2'); + cicdManager['queue']['add']('/path/to/repo1', { + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }, -1, false, true, 'hash3'); + + // Assert + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: true, + hash: 'hash1', + maximumStatuses: 1000 + }, + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: true, + hash: 'hash2', + maximumStatuses: 1000 + }, + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: (date.now + 1) * 1000, + attempts: 0, + detail: true, + hash: 'hash0', + maximumStatuses: 1000 + }, + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitHubV3 + }, + page: -1, + checkAfter: (date.now + 1) * 1000 + 1, + attempts: 0, + detail: true, + hash: 'hash3', + maximumStatuses: 1000 + } + ]); + }); + + }); + + + describe('GitLab', () => { + it('Should fetch a new cicd from GitLab (HTTPS Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, GitLabResponse, GitLabHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitLabCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitLabSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitLab (HTTPS Remote) with port', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com:80/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpsResponse(200, GitLabResponse, GitLabHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitLabCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'gitlab.com', + path: '/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100', + headers: { + 'User-Agent': 'vscode-git-graph' + }, + port: '80', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitLabSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://gitlab.com:80/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitLab (HTTP Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'http://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpResponse(200, JSON.stringify([ + { + id: 2211653232, + sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + ref: 'main', + status: 'success', + name: 'eslint-sast', + target_url: 'http://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + allow_failure: false + } + ]), GitLabHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '2211653232': { + name: 'eslint-sast', + status: 'success', + ref: 'main', + web_url: 'http://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', 2211653232, + { + name: 'eslint-sast', + status: 'success', + ref: 'main', + web_url: 'http://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for http://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)http://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for http://gitlab.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitLab (SSH Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'git@gitlab.com:keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (git@gitlab.com:keydepth/vscode-git-graph.git) for GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new cicd from GitLab (HTTPS Remote) not detail', async () => { + // Setup + jest.useFakeTimers(); + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, JSON.stringify([ + { + id: 740791415, + sha: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + ref: 'v1.31.0-beta.0', + status: 'success', + created_at: '2021-05-14T11:31:37.881Z', + updated_at: '2021-05-14T11:35:45.904Z', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/actions/runs/740791415' + } + ]), Object.assign({}, GitLabHeader, { + 'x-total': '1', + 'x-total-pages': '1' + })); + mockHttpsResponse(200, GitLabResponse, Object.assign({}, GitLabHeader, { + 'x-total': '1', + 'x-total-pages': '1' + })); + const cicdEvents = waitForEvents(cicdManager, 2, true); + cicdManager['queue']['queue'] = [{ + repo: '/path/to/repo1', + cicdConfig: { + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: false, + hash: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + maximumStatuses: 1000 + }]; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357'); + cicdManager['queue']['itemsAvailableCallback'](); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '740791415': { + name: '', + status: 'success', + ref: 'v1.31.0-beta.0', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/actions/runs/740791415', + event: '', + detail: false, + allow_failure: false + } + }, + 'hash': 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + 'repo': '/path/to/repo1' + }, GitLabCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'gitlab.com', + path: '/api/v4/projects/keydepth%2Fvscode-git-graph/pipelines?per_page=100', + headers: { + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357', 740791415, + { + name: '', + status: 'success', + ref: 'v1.31.0-beta.0', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/actions/runs/740791415', + event: '', + detail: false, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/pipelines?per_page=100 detail=false page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenCalledWith('GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/pipelines?per_page=100'); + expect(spyOnLog).toHaveBeenCalledWith('Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/b9112e60f5fb3d8bc2a387840577b4756a12f357/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenCalledWith('GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/b9112e60f5fb3d8bc2a387840577b4756a12f357/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenCalledWith('Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(6); + jest.useRealTimers(); + }); + + it('Should fetch a new cicd from GitLab (HTTPS Remote) not detail with no conclusion', async () => { + // Setup + jest.useFakeTimers(); + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, JSON.stringify([ + { + id: 740791415, + sha: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + ref: 'v1.31.0-beta.0', + status: 'pending', + created_at: '2021-05-14T11:31:37.881Z', + updated_at: '2021-05-14T11:35:45.904Z', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/actions/runs/740791415' + } + ]), Object.assign({}, GitLabHeader, { + 'x-total': '1', + 'x-total-pages': '1' + })); + mockHttpsResponse(200, JSON.stringify([ + { + id: 2211653232, + sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + ref: 'main', + status: 'pending', + name: 'eslint-sast', + target_url: 'https://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + allow_failure: false + } + ]), Object.assign({}, GitLabHeader, { + 'x-total': '1', + 'x-total-pages': '1' + })); + const cicdEvents = waitForEvents(cicdManager, 2, true); + cicdManager['queue']['queue'] = [{ + repo: '/path/to/repo1', + cicdConfig: { + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: false, + hash: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + maximumStatuses: 1000 + }]; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357'); + cicdManager['queue']['itemsAvailableCallback'](); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '740791415': { + name: '', + status: 'pending', + ref: 'v1.31.0-beta.0', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/actions/runs/740791415', + event: '', + detail: false, + allow_failure: false + } + }, + 'hash': 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + 'repo': '/path/to/repo1' + }, { + 'cicdDataSaves': { + '2211653232': { + name: 'eslint-sast', + status: 'pending', + ref: 'main', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'gitlab.com', + path: '/api/v4/projects/keydepth%2Fvscode-git-graph/pipelines?per_page=100', + headers: { + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357', 740791415, + { + name: '', + status: 'pending', + ref: 'v1.31.0-beta.0', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/actions/runs/740791415', + event: '', + detail: false, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/pipelines?per_page=100 detail=false page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenCalledWith('GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/pipelines?per_page=100'); + expect(spyOnLog).toHaveBeenCalledWith('Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/b9112e60f5fb3d8bc2a387840577b4756a12f357/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenCalledWith('GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/b9112e60f5fb3d8bc2a387840577b4756a12f357/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenCalledWith('Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=1(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(6); + jest.useRealTimers(); + }); + + it('Should fetch a new cicd from GitLab (HTTPS Remote) with ratelimit header', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, GitLabResponse, Object.assign({}, GitLabHeader, { + 'ratelimit-limit': '', + 'ratelimit-remaining': '', + 'ratelimit-reset': '' + })); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitLabCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitLabSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=None(every minute)/Remaining=None) from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitLab (HTTPS Remote) with x-page error header (number)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, GitLabResponse, Object.assign({}, GitLabHeader, { + 'x-page': 1 + })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from GitLab (HTTPS Remote) with x-total-pages error header (number)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, GitLabResponse, Object.assign({}, GitLabHeader, { + 'x-total-pages': 1 + })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from GitLab (HTTPS Remote) with x-total error header (number)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, GitLabResponse, Object.assign({}, GitLabHeader, { + 'x-total': 1 + })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from GitLab (HTTPS Remote) no emmit', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, GitLabResponse, GitLabHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new cicd from GitLab (URL is Not Match)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'ftp://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (ftp://gitlab.com/keydepth/vscode-git-graph.git) for GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new cicd from GitLab (Bad URL)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (gitlab.com/keydepth/vscode-git-graph.git) for GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new cicd from GitLab (No target_url)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, JSON.stringify([ + { + id: 2211653232, + sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + ref: 'main', + status: 'success', + name: 'eslint-sast', + allow_failure: false + } + ]), GitLabHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '2211653232': { + name: 'eslint-sast', + status: 'success', + ref: 'main', + web_url: 'https://gitlab.com/keydepth/vscode-git-graph/-/jobs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', 2211653232, + { + name: 'eslint-sast', + status: 'success', + ref: 'main', + web_url: 'https://gitlab.com/keydepth/vscode-git-graph/-/jobs/2211653232', + event: '', + detail: true, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should fetch a new multiple cicd from GitLab', async () => { + // Setup + jest.useFakeTimers(); + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + for (let i = 0; i < 10; i++) { + mockHttpsResponse(200, GitLabResponse, GitLabHeader); + } + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([GitLabCicdEvents]); + expect(cicdManager['queue']['queue'].length).toBe(9); + expect(cicdManager['queue']['queue'][0].attempts).toBe(0); + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + jest.runAllTimers(); + for (let i = 1; i < 10; i++) { + // jest.runOnlyPendingTimers(); + expect(spyOnLog).toHaveBeenNthCalledWith(3 + i * 2, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100&page=' + (i + 1) + ' detail=true page=' + (i + 1) + ' from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(4 + i * 2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100&page=' + (i + 1)); + } + jest.useRealTimers(); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...GitLabSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(22); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo', async () => { + // Setup + const cicdEvents = waitForEvents(cicdManager, 1); + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, GitLabResponse, GitLabHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo', 'hash0'); + + // Assert + expect.assertions(2); + if (data) { + expect(JSON.parse(data)).toStrictEqual({ + 'id0': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: false + } + }); + } + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '2211653232': { + name: 'eslint-sast', + status: 'success', + ref: 'main', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo' + }]); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with cicdToken', async () => { + // Setup + const cicdEvents = waitForEvents(cicdManager, 1); + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, 'nonce', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: 'aaaaaa' + }]) + }); + mockHttpsResponse(200, GitLabResponse, GitLabHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash0'); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '2211653232': { + name: 'eslint-sast', + status: 'success', + ref: 'main', + web_url: 'https://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'gitlab.com', + path: '/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/hash0/statuses?per_page=100', + headers: { + 'User-Agent': 'vscode-git-graph', + 'PRIVATE-TOKEN': 'aaaaaa' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with responce is empty JSON', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, JSON.stringify({}), GitLabHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo', 'hash0'); + + // Assert + expect.assertions(1); + if (data) { + expect(JSON.parse(data)).toStrictEqual({ + 'id0': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: false + } + }); + } + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with responce is Not JSON format', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, 'Not JSON format', GitLabHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect.assertions(5); + expect(data).toStrictEqual(null); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'GitLab API Error - (200)API Result error. : Unexpected token N in JSON at position 0'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should halt fetching the cicd when a known cicd is requested repo with responce is No id JSON format', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, JSON.stringify([ + { + sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + ref: 'main', + status: 'success', + name: 'eslint-sast', + target_url: 'https://gitlab.com/mhutchie/vscode-git-graph/runs/2211653232', + allow_failure: false + } + ]), GitLabHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + }); + + it('Should halt fetching the cicd when the GitLab cicd url request is unsuccessful with Message Body', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(404, '{"message":"Error Message Body"}', GitLabHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API Error - (404)Error Message Body'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should halt fetching the cicd when the GitLab cicd url request is unsuccessful with No Message Body', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(404, '{}', GitLabHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API Error - (404)undefined'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should requeue the request when the GitLab API cannot find the commit', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(200, JSON.stringify([]), GitLabHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(GitLabHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API - (200)https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'CICD Maximum Statuses(maximumStatuses=1000) reached, if you want to change Maximum page, please configure git-graph.repository.commits.fetchCICDsMaximumStatuses'); + expect(spyOnLog).toHaveBeenNthCalledWith(4, 'Added CICD for https://gitlab.com/keydepth/vscode-git-graph.git last_page=10(RateLimit=60(every minute)/Remaining=57/' + new Date(1618343683).toString() + ') from GitLab'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + + }); + + it('Should set the GitLab API timeout and requeue the request when the rate limit is reached with cicdToken', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: 'aaaaaa' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpsResponse(429, '', Object.assign({}, GitLabHeader, { 'ratelimit-remaining': '0', 'ratelimit-reset': (date.now + 1).toString() })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('GitLab API Rate Limit Reached - Paused fetching from GitLab until the Rate Limit is reset (RateLimit=60(every minute)/' + new Date(1618343683).toString() + ')'); + }); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + attempts: 0, + checkAfter: 1587559259000, + cicdConfig: { + cicdToken: 'aaaaaa', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000, + page: -1, + repo: '/path/to/repo1' + } + ]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'gitlab.com', + path: '/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100', + headers: { + 'User-Agent': 'vscode-git-graph', + 'PRIVATE-TOKEN': 'aaaaaa' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://gitlab.com/api/v4/projects/keydepth%2Fvscode-git-graph/repository/commits/149ecc50e5c223251f80a0223cfbbd9822307224/statuses?per_page=100 detail=true page=-1 from GitLab'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'GitLab API Rate Limit Reached - Paused fetching from GitLab until the Rate Limit is reset (RateLimit=60(every minute)/' + new Date(1618343683).toString() + ')'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + expect(cicdManager['gitLabTimeout']).toBe((date.now + 1) * 1000); + }); + + it('Should set the GitLab API timeout and requeue the request when the API returns a 5xx error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsResponse(500, '', GitLabHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitLabTimeout']).toBe(date.now * 1000 + 600000); + }); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + page: -1, + checkAfter: date.now * 1000 + 600000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the GitLab API timeout and requeue the request when there is an HTTPS Client Request Error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsClientRequestErrorEvent({ message: 'Error Message' }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitLabTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('GitLab API https Error - Error Message'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the GitLab API timeout and requeue the request when there is an HTTPS Incoming Message Error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsIncomingMessageErrorEvent(); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitLabTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('GitLab API https Error - undefined'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the GitLab API timeout and requeue the request once when there are multiple HTTPS Error Events', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + mockHttpsMultipleErrorEvents(); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['gitLabTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('GitLab API https Error - undefined'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should requeue the request when it\'s before the GitLab API timeout', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(GitLabGetRspos); + cicdManager['gitLabTimeout'] = (date.now + 1) * 1000; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + page: -1, + checkAfter: (date.now + 1) * 1000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + }); + + it('Should insert requests into the priority queue in the correct order', async () => { + // Setup + spyOnGetRepos.mockReturnValue({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }]) + }); + mockHttpsResponse(403, '', Object.assign({}, GitLabHeader, { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': (date.now + 1).toString() })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash0'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('GitLab API Error - (403)undefined'); + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash1'); + await cicdManager.getCICDDetail('/path/to/repo1', 'hash2'); + await cicdManager.getCICDDetail('/path/to/repo1', 'hash2'); + cicdManager['queue']['add']('/path/to/repo1', { + provider: CICDProvider.GitLabV4, + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + cicdToken: '' + }, -1, false, true, 'hash3'); + + // Assert + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: true, + hash: 'hash1', + maximumStatuses: 1000 + }, + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: true, + hash: 'hash2', + maximumStatuses: 1000 + }, + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://gitlab.com/keydepth/vscode-git-graph.git', + provider: CICDProvider.GitLabV4 + }, + page: -1, + checkAfter: 1, + attempts: 0, + detail: true, + hash: 'hash3', + maximumStatuses: 1000 + } + ]); + }); + }); + + + + describe('Jenkins', () => { + it('Should fetch a new cicd from Jenkins (HTTPS Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JenkinsResponse, JenkinsHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([JenkinsCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...JenkinsSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from Jenkins (HTTPS Remote) with port', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net:80/job/job01/job/MultiBranch/job/master/3/', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpsResponse(200, JenkinsResponse, JenkinsHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([JenkinsCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'jenkins.net', + path: '/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]', + headers: { + 'User-Agent': 'vscode-git-graph' + }, + port: '80', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith(...JenkinsSaveCicd); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from Jenkins (HTTP Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'http://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpResponse(200, JSON.stringify( + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowJob', + builds: [ + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowRun', + actions: [ + { + _class: 'hudson.plugins.git.util.BuildData', + lastBuiltRevision: { + branch: [ + { + SHA1: '149ecc50e5c223251f80a0223cfbbd9822307224', + name: 'master' + } + ] + } + } + ], + fullDisplayName: 'job01 » MultiBranch » master #3', + id: '3', + result: 'SUCCESS', + timestamp: 1620716982997, + url: 'http://jenkins.net/job/job01/job/MultiBranch/job/master/3/' + } + ] + } + ), JenkinsHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '3': { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'http://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', '3', + { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'http://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for http://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)http://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from Jenkins (SSH Remote)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'git@jenkins.net:keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (git@jenkins.net:keydepth/vscode-git-graph.git) for Jenkins'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new cicd from Jenkins (HTTPS Remote) not detail', async () => { + // Setup + jest.useFakeTimers(); + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JSON.stringify( + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowJob', + builds: [ + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowRun', + actions: [ + { + _class: 'hudson.plugins.git.util.BuildData', + lastBuiltRevision: { + branch: [ + { + SHA1: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + name: 'master' + } + ] + } + } + ], + fullDisplayName: 'job01 » MultiBranch » master #3', + id: '3', + result: 'SUCCESS', + timestamp: 1620716982997, + url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/' + } + ] + } + ), Object.assign({}, JenkinsHeader, { + 'x-total': '1', + 'x-total-pages': '1' + })); + mockHttpsResponse(200, JenkinsResponse, Object.assign({}, JenkinsHeader, { + 'x-total': '1', + 'x-total-pages': '1' + })); + const cicdEvents = waitForEvents(cicdManager, 2, true); + cicdManager['queue']['queue'] = [{ + repo: '/path/to/repo1', + cicdConfig: { + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job02/job/MultiBranch/job/master/3/', + cicdToken: '' + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: false, + hash: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + maximumStatuses: 1000 + }]; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357'); + cicdManager['queue']['itemsAvailableCallback'](); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '3': { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: false, + allow_failure: false + } + }, + 'hash': 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + 'repo': '/path/to/repo1' + }, JenkinsCicdEvents]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'jenkins.net', + path: '/job/job02/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]', + headers: { + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', '3', + { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + jest.useRealTimers(); + }); + + it('Should fetch a new cicd from Jenkins (HTTPS Remote) not detail with no conclusion', async () => { + // Setup + jest.useFakeTimers(); + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JSON.stringify( + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowJob', + builds: [ + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowRun', + actions: [ + { + _class: 'hudson.plugins.git.util.BuildData', + lastBuiltRevision: { + branch: [ + { + SHA1: '149ecc50e5c223251f80a0223cfbbd9822307224', + name: 'master' + } + ] + } + } + ], + fullDisplayName: 'job02 » MultiBranch » master #23', + id: '23', + result: 'pending', + timestamp: 1620716982997, + url: 'https://jenkins.net/job/job02/job/MultiBranch/job/master/23/' + } + ] + } + ), Object.assign({}, JenkinsHeader, { + 'x-total': '1', + 'x-total-pages': '1' + })); + mockHttpsResponse(200, JenkinsResponse, Object.assign({}, JenkinsHeader, { + 'x-total': '1', + 'x-total-pages': '1' + })); + const cicdEvents = waitForEvents(cicdManager, 2, true); + cicdManager['queue']['queue'] = [{ + repo: '/path/to/repo1', + cicdConfig: { + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job02/job/MultiBranch/job/master/23/', + cicdToken: '' + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: false, + hash: 'b9112e60f5fb3d8bc2a387840577b4756a12f357', + maximumStatuses: 1000 + }]; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'b9112e60f5fb3d8bc2a387840577b4756a12f357'); + cicdManager['queue']['itemsAvailableCallback'](); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '23': { + name: 'job02 » MultiBranch » master #23', + status: 'pending', + ref: 'master', + web_url: 'https://jenkins.net/job/job02/job/MultiBranch/job/master/23/', + event: '', + detail: false, + allow_failure: false + }, + '3': { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }, { + 'cicdDataSaves': { + '23': { + name: 'job02 » MultiBranch » master #23', + status: 'pending', + ref: 'master', + web_url: 'https://jenkins.net/job/job02/job/MultiBranch/job/master/23/', + event: '', + detail: false, + allow_failure: false + }, + '3': { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'jenkins.net', + path: '/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]', + headers: { + 'User-Agent': 'vscode-git-graph' + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', '23', + { + name: 'job02 » MultiBranch » master #23', + status: 'pending', + ref: 'master', + web_url: 'https://jenkins.net/job/job02/job/MultiBranch/job/master/23/', + event: '', + detail: false, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledWith('Requesting CICD for https://jenkins.net/job/job02/job/MultiBranch/job/master/23/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API - (200)https://jenkins.net/job/job02/job/MultiBranch/job/master/23/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(4); + jest.useRealTimers(); + }); + + it('Should fetch a new cicd from Jenkins (HTTPS Remote) with x-jenkins error header (number)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JenkinsResponse, Object.assign({}, JenkinsHeader, { + 'x-jenkins': '1.1.1' + })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from Jenkins (HTTPS Remote) with builds format error header (number)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JSON.stringify( + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowJob', + builds: {} + } + ), JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from Jenkins (HTTPS Remote) with lastBuiltRevision format error header (number)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JSON.stringify( + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowJob', + builds: [ + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowRun', + actions: [ + { + _class: 'hudson.plugins.git.util.BuildData' + } + ], + fullDisplayName: 'job01 » MultiBranch » master #3', + id: '3', + result: 'SUCCESS', + timestamp: 1620716982997, + url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/' + } + ] + } + ), JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from Jenkins (HTTPS Remote) no emmit', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JenkinsResponse, JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should fetch a new cicd from Jenkins (URL is Not Match)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'ftp://jenkins.net/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (ftp://jenkins.net/keydepth/vscode-git-graph.git) for Jenkins'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new cicd from Jenkins (Bad URL)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'jenkins.net/keydepth/vscode-git-graph.git', + cicdToken: '' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD is not match URL (jenkins.net/keydepth/vscode-git-graph.git) for Jenkins'); + expect(spyOnLog).toHaveBeenCalledTimes(1); + }); + + it('Should fetch a new cicd from Jenkins (No url)', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JSON.stringify( + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowJob', + builds: [ + { + _class: 'org.jenkinsci.plugins.workflow.job.WorkflowRun', + actions: [ + { + _class: 'hudson.plugins.git.util.BuildData', + lastBuiltRevision: { + branch: [ + { + SHA1: '149ecc50e5c223251f80a0223cfbbd9822307224', + name: 'master' + } + ] + } + } + ], + fullDisplayName: 'job01 » MultiBranch » master #3', + id: '3', + result: 'SUCCESS', + timestamp: 1620716982997 + } + ] + } + ), JenkinsHeader); + const cicdEvents = waitForEvents(cicdManager, 1); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '3': { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: '', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnSaveCicd).toHaveBeenCalledWith( + '/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224', '3', + { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: '', + event: '', + detail: true, + allow_failure: false + }); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo', async () => { + // Setup + const cicdEvents = waitForEvents(cicdManager, 1); + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, JenkinsResponse, JenkinsHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo', 'hash0'); + + // Assert + expect.assertions(2); + if (data) { + expect(JSON.parse(data)).toStrictEqual({ + 'id0': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: false + } + }); + } + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '3': { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo' + }]); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with cicdToken', async () => { + // Setup + const cicdEvents = waitForEvents(cicdManager, 1); + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, 'nonce', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: 'user:aaaaaa' + }]) + }); + mockHttpsResponse(200, JenkinsResponse, JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash0'); + + // Assert + expect(await cicdEvents).toStrictEqual([{ + 'cicdDataSaves': { + '3': { + name: 'job01 » MultiBranch » master #3', + status: 'SUCCESS', + ref: 'master', + web_url: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + event: '', + detail: true, + allow_failure: false + } + }, + 'hash': '149ecc50e5c223251f80a0223cfbbd9822307224', + 'repo': '/path/to/repo1' + }]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'jenkins.net', + path: '/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]', + headers: { + 'User-Agent': 'vscode-git-graph', + 'Authorization': 'Basic ' + new Buffer('user:aaaaaa').toString('base64') + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with responce is empty JSON', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, JSON.stringify({}), JenkinsHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo', 'hash0'); + + // Assert + expect.assertions(5); + if (data) { + expect(JSON.parse(data)).toStrictEqual({ + 'id0': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: false + } + }); + } + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'Jenkins API Error - (200)API Result error. : Cannot read property \'length\' of undefined'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + + }); + + it('Should trigger the cicd to be emitted when a known cicd is requested repo with responce is Not JSON format', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, 'hash0', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: '' + }]) + }); + mockHttpsResponse(200, 'Not JSON format', JenkinsHeader); + + // Run + let data = await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect.assertions(5); + expect(data).toStrictEqual(null); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'Jenkins API Error - (200)API Result error. : Unexpected token N in JSON at position 0'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should halt fetching the cicd when a known cicd is requested repo with responce is No id JSON format', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JSON.stringify([ + { + sha: '149ecc50e5c223251f80a0223cfbbd9822307224', + ref: 'main', + status: 'success', + name: 'eslint-sast', + target_url: 'https://jenkins.net/mhutchie/vscode-git-graph/runs/2211653232', + allow_failure: false + } + ]), JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'Jenkins API Error - (200)API Result error. : Cannot read property \'length\' of undefined'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + }); + + it('Should halt fetching the cicd when the Jenkins cicd url request is unsuccessful with Message Body', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(404, '{"message":"Error Message Body"}', JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API Error - (404)Error Message Body'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should halt fetching the cicd when the Jenkins cicd url request is unsuccessful with No Message Body', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(404, '{}', JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API Error - (404)undefined'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + }); + + it('Should requeue the request when the Jenkins API cannot find the commit', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(200, JSON.stringify([]), JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + expect(spyOnHttpsGet).toHaveBeenCalledWith(JenkinsHttpsGet, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API - (200)https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]'); + expect(spyOnLog).toHaveBeenNthCalledWith(3, 'Jenkins API Error - (200)API Result error. : Cannot read property \'length\' of undefined'); + expect(spyOnLog).toHaveBeenCalledTimes(3); + + }); + + it('Should set the Jenkins API timeout and requeue the request when the rate limit is reached with cicdToken', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: 'user:aaaaaa' + }]), + '/path/to/repo2': mockRepoState('Custom Name', 0, null, null) + }); + mockHttpsResponse(429, '', Object.assign({}, JenkinsHeader, { 'ratelimit-remaining': '0', 'ratelimit-reset': (date.now + 1).toString() })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API Rate Limit Reached - Paused fetching from Jenkins until the Rate Limit is reset (RateLimit=undefined(every minute)/' + new Date(1618343683).toString() + ')'); + }); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + attempts: 0, + checkAfter: 1587559259000, + cicdConfig: { + cicdToken: 'user:aaaaaa', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000, + page: -1, + repo: '/path/to/repo1' + } + ]); + expect(spyOnHttpsGet).toHaveBeenCalledWith({ + hostname: 'jenkins.net', + path: '/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]]', + headers: { + 'User-Agent': 'vscode-git-graph', + 'Authorization': 'Basic ' + new Buffer('user:aaaaaa').toString('base64') + }, + port: '', + agent: false, + timeout: 15000 + }, expect.anything()); + expect(spyOnLog).toHaveBeenNthCalledWith(1, 'Requesting CICD for https://jenkins.net/job/job01/job/MultiBranch/job/master/3/api/json?tree=builds[id,timestamp,fullDisplayName,result,url,actions[lastBuiltRevision[branch[*]]]] page=-1 from Jenkins'); + expect(spyOnLog).toHaveBeenNthCalledWith(2, 'Jenkins API Rate Limit Reached - Paused fetching from Jenkins until the Rate Limit is reset (RateLimit=undefined(every minute)/' + new Date(1618343683).toString() + ')'); + expect(spyOnLog).toHaveBeenCalledTimes(2); + expect(cicdManager['jenkinsTimeout']).toBe((date.now + 1) * 1000); + }); + + it('Should set the Jenkins API timeout and requeue the request when the API returns a 5xx error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsResponse(500, '', JenkinsHeader); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['jenkinsTimeout']).toBe(date.now * 1000 + 600000); + }); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + page: -1, + checkAfter: date.now * 1000 + 600000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the Jenkins API timeout and requeue the request when there is an HTTPS Client Request Error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsClientRequestErrorEvent({ message: 'Error Message' }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['jenkinsTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API https Error - Error Message'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the Jenkins API timeout and requeue the request when there is an HTTPS Incoming Message Error', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsIncomingMessageErrorEvent(); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['jenkinsTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API https Error - undefined'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should set the Jenkins API timeout and requeue the request once when there are multiple HTTPS Error Events', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + mockHttpsMultipleErrorEvents(); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['jenkinsTimeout']).toBe(date.now * 1000 + 300000); + }); + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API https Error - undefined'); + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + page: -1, + checkAfter: date.now * 1000 + 300000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + + it('Should requeue the request when it\'s before the Jenkins API timeout', async () => { + // Setup + spyOnGetRepos.mockReturnValueOnce(JenkinsGetRspos); + cicdManager['jenkinsTimeout'] = (date.now + 1) * 1000; + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', '149ecc50e5c223251f80a0223cfbbd9822307224'); + + // Assert + await waitForExpect(() => { + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + page: -1, + checkAfter: (date.now + 1) * 1000, + attempts: 0, + detail: true, + hash: '149ecc50e5c223251f80a0223cfbbd9822307224', + maximumStatuses: 1000 + } + ]); + }); + }); + + it('Should insert requests into the priority queue in the correct order', async () => { + // Setup + spyOnGetRepos.mockReturnValue({ + '/path/to/repo1': mockRepoState(null, 0, + 'ElZJNHSyT6JDjOGDaVaPiZenu3Xu2MZf', [{ + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: '' + }]) + }); + mockHttpsResponse(403, '', Object.assign({}, JenkinsHeader, { 'x-ratelimit-remaining': '0', 'x-ratelimit-reset': (date.now + 1).toString() })); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash0'); + + // Assert + await waitForExpect(() => { + expect(spyOnLog).toHaveBeenCalledWith('Jenkins API Error - (403)undefined'); + }); + + // Run + await cicdManager.getCICDDetail('/path/to/repo1', 'hash1'); + await cicdManager.getCICDDetail('/path/to/repo1', 'hash2'); + await cicdManager.getCICDDetail('/path/to/repo1', 'hash2'); + cicdManager['queue']['add']('/path/to/repo1', { + provider: CICDProvider.JenkinsV2, + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + cicdToken: '' + }, -1, false, true, 'hash3'); + + // Assert + expect(cicdManager['queue']['queue']).toStrictEqual([ + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: true, + hash: 'hash1', + maximumStatuses: 1000 + }, + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + page: -1, + checkAfter: 0, + attempts: 0, + detail: true, + hash: 'hash2', + maximumStatuses: 1000 + }, + { + repo: '/path/to/repo1', + cicdConfig: { + cicdToken: '', + cicdUrl: 'https://jenkins.net/job/job01/job/MultiBranch/job/master/3/', + provider: CICDProvider.JenkinsV2 + }, + page: -1, + checkAfter: 1, + attempts: 0, + detail: true, + hash: 'hash3', + maximumStatuses: 1000 + } + ]); + }); + }); + + }); + + + describe('clearCache', () => { + let spyOnClearCICDCache: jest.SpyInstance; + beforeAll(() => { + spyOnClearCICDCache = jest.spyOn(extensionState, 'clearCICDCache'); + }); + + it('Should clear the cache of cicd', async () => { + // Setup + spyOnClearCICDCache.mockResolvedValueOnce(null); + + // Run + cicdManager.clearCache(); + + // Assert + expect(cicdManager['cicds']).toStrictEqual({}); + expect(spyOnClearCICDCache).toHaveBeenCalledTimes(1); + }); + + }); +}); + + +function mockHttpsResponse(statusCode: number, resData: string, headers: { [header: string]: string | number } = {}) { + spyOnHttpsGet.mockImplementationOnce((_: string | https.RequestOptions | URL, callback: (res: IncomingMessage) => void): ClientRequest => { + if (callback) { + const callbacks: { [event: string]: (...args: any) => void } = {}; + const message: IncomingMessage = { + statusCode: statusCode, + headers: Object.assign({}, headers), + on: (event: string, listener: () => void) => { + callbacks[event] = listener; + return message; + } + }; + callback(message); + callbacks['data'](Buffer.from(resData)); + callbacks['end'](); + } + return ({ + on: jest.fn() + }) as any as ClientRequest; + }); +} + +function mockHttpResponse(statusCode: number, resData: string, headers: { [header: string]: string } = {}) { + spyOnHttpGet.mockImplementationOnce((_: string | http.RequestOptions | URL, callback: (res: IncomingMessage) => void): ClientRequest => { + if (callback) { + const callbacks: { [event: string]: (...args: any) => void } = {}; + const message: IncomingMessage = { + statusCode: statusCode, + headers: Object.assign({}, headers), + on: (event: string, listener: () => void) => { + callbacks[event] = listener; + return message; + } + }; + callback(message); + callbacks['data'](Buffer.from(resData)); + callbacks['end'](); + } + return ({ + on: jest.fn() + }) as any as ClientRequest; + }); +} + +function mockHttpsClientRequestErrorEvent(err: any) { + spyOnHttpsGet.mockImplementationOnce((_1: string | https.RequestOptions | URL, _2: (res: IncomingMessage) => void): ClientRequest => { + const request: ClientRequest = { + on: (event: string, callback: (err: any) => void) => { + if (event === 'error') { + callback(err); + } + return request; + } + }; + return request; + }); +} + +function mockHttpsIncomingMessageErrorEvent() { + spyOnHttpsGet.mockImplementationOnce((_: string | https.RequestOptions | URL, callback: (res: IncomingMessage) => void): ClientRequest => { + const callbacks: { [event: string]: (...args: any) => void } = {}; + const message: IncomingMessage = { + on: (event: string, listener: () => void) => { + callbacks[event] = listener; + return message; + } + }; + callback(message); + callbacks['error'](); + return ({ + on: jest.fn() + }) as any as ClientRequest; + }); +} + +function mockHttpsMultipleErrorEvents() { + spyOnHttpsGet.mockImplementationOnce((_: string | https.RequestOptions | URL, callback: (res: IncomingMessage) => void): ClientRequest => { + const callbacks: { [event: string]: (...args: any) => void } = {}; + const message: IncomingMessage = { + on: (event: string, listener: () => void) => { + callbacks[event] = listener; + return message; + } + }; + callback(message); + callbacks['error'](); + + const request: ClientRequest = { + on: (event: string, callback: () => void) => { + if (event === 'error') { + callback(); + } + return request; + } + }; + return request; + }); +} + +function waitForEvents(cicdManager: CicdManager, n: number, runPendingTimers = false) { + return new Promise((resolve) => { + const events: CICDEvent[] = []; + cicdManager.onCICD((event) => { + events.push(event); + if (runPendingTimers) { + jest.runOnlyPendingTimers(); + } + if (events.length === n) { + resolve(events); + } + }); + }); +} + +function mockRepoState(name: string | null, workspaceFolderIndex: number | null, cicdNonce: string | null, cicdConfigs: CICDConfig[] | null) { + return Object.assign({}, DEFAULT_REPO_STATE, { name: name, workspaceFolderIndex: workspaceFolderIndex, cicdNonce: cicdNonce, cicdConfigs: cicdConfigs }); +} + diff --git a/tests/commands.test.ts b/tests/commands.test.ts index 921105ba..e33f88eb 100644 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -21,6 +21,7 @@ import { RepoManager } from '../src/repoManager'; import { GitFileStatus, RepoDropdownOrder } from '../src/types'; import * as utils from '../src/utils'; import { EventEmitter } from '../src/utils/event'; +import { CicdManager } from '../src/cicdManager'; import { waitForExpect } from './helpers/expectations'; import { mockRepoState } from './helpers/utils'; @@ -31,6 +32,7 @@ let logger: Logger; let dataSource: DataSource; let extensionState: ExtensionState; let avatarManager: AvatarManager; +let cicdManager: CicdManager; let repoManager: RepoManager; let spyOnGitGraphViewCreateOrShow: jest.SpyInstance, spyOnGetRepos: jest.SpyInstance, spyOnGetKnownRepo: jest.SpyInstance, spyOnRegisterRepo: jest.SpyInstance, spyOnGetCodeReviews: jest.SpyInstance, spyOnEndCodeReview: jest.SpyInstance, spyOnGetCommitSubject: jest.SpyInstance, spyOnLog: jest.SpyInstance, spyOnLogError: jest.SpyInstance; beforeAll(() => { @@ -41,6 +43,7 @@ beforeAll(() => { extensionState = new ExtensionState(vscode.mocks.extensionContext, onDidChangeGitExecutable.subscribe); avatarManager = new AvatarManager(dataSource, extensionState, logger); repoManager = new RepoManager(dataSource, extensionState, onDidChangeConfiguration.subscribe, logger); + cicdManager = new CicdManager(extensionState, repoManager, logger); spyOnGitGraphViewCreateOrShow = jest.spyOn(GitGraphView, 'createOrShow'); spyOnGetRepos = jest.spyOn(repoManager, 'getRepos'); spyOnGetKnownRepo = jest.spyOn(repoManager, 'getKnownRepo'); @@ -55,6 +58,7 @@ beforeAll(() => { afterAll(() => { repoManager.dispose(); avatarManager.dispose(); + cicdManager.dispose(); extensionState.dispose(); dataSource.dispose(); logger.dispose(); @@ -65,7 +69,7 @@ afterAll(() => { describe('CommandManager', () => { let commandManager: CommandManager; beforeEach(() => { - commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, cicdManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); }); afterEach(() => { commandManager.dispose(); @@ -73,7 +77,7 @@ describe('CommandManager', () => { it('Should construct a CommandManager, and be disposed', () => { // Assert - expect(commandManager['disposables']).toHaveLength(11); + expect(commandManager['disposables']).toHaveLength(12); expect(commandManager['gitExecutable']).toStrictEqual({ path: '/path/to/git', version: '2.25.0' @@ -109,7 +113,7 @@ describe('CommandManager', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, cicdManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); // Assert await waitForExpect(() => { @@ -126,7 +130,7 @@ describe('CommandManager', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, cicdManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); // Assert await waitForExpect(() => { @@ -143,7 +147,7 @@ describe('CommandManager', () => { vscode.commands.executeCommand.mockRejectedValueOnce(null); // Run - commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, cicdManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); // Assert await waitForExpect(() => { @@ -164,7 +168,7 @@ describe('CommandManager', () => { }); // Run - commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, cicdManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); // Assert await waitForExpect(() => { @@ -185,7 +189,7 @@ describe('CommandManager', () => { // Assert await waitForExpect(() => { expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.view'); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); }); }); @@ -200,7 +204,7 @@ describe('CommandManager', () => { // Assert await waitForExpect(() => { expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.view'); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/workspace-folder/repo' }); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/workspace-folder/repo' }); }); }); @@ -216,7 +220,7 @@ describe('CommandManager', () => { // Assert await waitForExpect(() => { expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.view'); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/workspace-folder/repo' }); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/workspace-folder/repo' }); }); }); @@ -232,7 +236,7 @@ describe('CommandManager', () => { // Assert await waitForExpect(() => { expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.view'); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/workspace-folder' }); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/workspace-folder' }); }); }); }); @@ -574,6 +578,26 @@ describe('CommandManager', () => { }); }); + describe('git-graph.clearCICDCache', () => { + let spyOnClearCache: jest.SpyInstance; + beforeAll(() => { + spyOnClearCache = jest.spyOn(cicdManager, 'clearCache'); + }); + + it('Should clear the cicd cache', async () => { + // Setup + spyOnClearCache.mockResolvedValueOnce(null); + + // Run + vscode.commands.executeCommand('git-graph.clearCICDCache'); + + // Assert + await waitForExpect(() => { + expect(spyOnClearCache).toBeCalledTimes(1); + }); + }); + }); + describe('git-graph.fetch', () => { let spyOnGetLastActiveRepo: jest.SpyInstance; beforeAll(() => { @@ -622,7 +646,7 @@ describe('CommandManager', () => { canPickMany: false } ); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo1', runCommandOnLoad: 'fetch' }); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo1', runCommandOnLoad: 'fetch' }); }); }); @@ -660,7 +684,7 @@ describe('CommandManager', () => { canPickMany: false } ); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo1', runCommandOnLoad: 'fetch' }); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo1', runCommandOnLoad: 'fetch' }); }); }); @@ -698,7 +722,7 @@ describe('CommandManager', () => { canPickMany: false } ); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo1', runCommandOnLoad: 'fetch' }); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo1', runCommandOnLoad: 'fetch' }); }); }); @@ -786,7 +810,7 @@ describe('CommandManager', () => { // Assert await waitForExpect(() => { expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.fetch'); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo1', runCommandOnLoad: 'fetch' }); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo1', runCommandOnLoad: 'fetch' }); }); }); @@ -800,7 +824,7 @@ describe('CommandManager', () => { // Assert await waitForExpect(() => { expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.fetch'); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); }); }); }); @@ -1033,7 +1057,7 @@ describe('CommandManager', () => { // Assert await waitForExpect(() => { expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.resumeWorkspaceCodeReview'); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo', commitDetails: { commitHash: '2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c', @@ -1090,7 +1114,7 @@ describe('CommandManager', () => { // Assert await waitForExpect(() => { expect(spyOnLog).toHaveBeenCalledWith('Command Invoked: git-graph.resumeWorkspaceCodeReview'); - expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { + expect(spyOnGitGraphViewCreateOrShow).toHaveBeenCalledWith('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo', commitDetails: { commitHash: '2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c', diff --git a/tests/extensionState.test.ts b/tests/extensionState.test.ts index e670c61e..b2c39aca 100644 --- a/tests/extensionState.test.ts +++ b/tests/extensionState.test.ts @@ -5,7 +5,9 @@ jest.mock('fs'); import * as fs from 'fs'; import { ExtensionState } from '../src/extensionState'; -import { BooleanOverride, FileViewType, GitGraphViewGlobalState, GitGraphViewWorkspaceState, GitRepoState, RepoCommitOrdering } from '../src/types'; +import { BooleanOverride, CICDDataSave, CICDProvider, FileViewType, GitGraphViewGlobalState, GitGraphViewWorkspaceState, GitRepoState, RepoCommitOrdering } from '../src/types'; +import * as utils from '../src/utils'; +import * as crypto from 'crypto'; import { GitExecutable } from '../src/utils'; import { EventEmitter } from '../src/utils/event'; @@ -71,6 +73,8 @@ describe('ExtensionState', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Enabled, onRepoLoadShowSpecificBranches: ['master'], pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Enabled, showStashes: BooleanOverride.Enabled, @@ -119,6 +123,8 @@ describe('ExtensionState', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, @@ -157,6 +163,8 @@ describe('ExtensionState', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, @@ -195,6 +203,8 @@ describe('ExtensionState', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: false, showRemoteBranchesV2: BooleanOverride.Disabled, showStashes: BooleanOverride.Default, @@ -233,6 +243,8 @@ describe('ExtensionState', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: false, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, @@ -271,6 +283,8 @@ describe('ExtensionState', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Enabled, showStashes: BooleanOverride.Default, @@ -312,6 +326,8 @@ describe('ExtensionState', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, @@ -333,6 +349,8 @@ describe('ExtensionState', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: false, showRemoteBranchesV2: BooleanOverride.Disabled, showStashes: BooleanOverride.Default, @@ -353,6 +371,193 @@ describe('ExtensionState', () => { // Assert expect(result).toStrictEqual({}); }); + + it('Should return the stored repositories with decrypt', () => { + // Setup + const cicdConfigsIn = [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '31613262336334643565366631613262:aedfce60882614d275f01b86aa151bcd' + }]; + const cicdConfigsOut = [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: 'token' + }]; + const repoState: GitRepoState = { + cdvDivider: 0.5, + cdvHeight: 250, + columnWidths: null, + commitOrdering: RepoCommitOrdering.AuthorDate, + fileViewType: FileViewType.List, + hideRemotes: [], + includeCommitsMentionedByReflogs: BooleanOverride.Enabled, + issueLinkingConfig: null, + lastImportAt: 0, + name: 'Custom Name', + onlyFollowFirstParent: BooleanOverride.Disabled, + onRepoLoadShowCheckedOutBranch: BooleanOverride.Enabled, + onRepoLoadShowSpecificBranches: ['master'], + pullRequestConfig: null, + cicdConfigs: cicdConfigsIn, + cicdNonce: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d', + showRemoteBranches: true, + showRemoteBranchesV2: BooleanOverride.Enabled, + showStashes: BooleanOverride.Enabled, + showTags: BooleanOverride.Enabled, + workspaceFolderIndex: 0 + }; + extensionContext.workspaceState.get.mockReturnValueOnce({ + '/path/to/repo': repoState + }); + + // Run + const result = extensionState.getRepos(); + + // Assert + expect(result).toStrictEqual({ + '/path/to/repo': Object.assign({}, repoState, { + cicdConfigs: cicdConfigsOut, + cicdNonce: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d' + }) + }); + }); + + it('Should return the stored repositories with decrypt cicdNonce is null', () => { + // Setup + const cicdConfigsIn = [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '31613262336334643565366631613262:aedfce60882614d275f01b86aa151bcd' + }]; + const cicdConfigsOut: any = []; + const repoState: GitRepoState = { + cdvDivider: 0.5, + cdvHeight: 250, + columnWidths: null, + commitOrdering: RepoCommitOrdering.AuthorDate, + fileViewType: FileViewType.List, + hideRemotes: [], + includeCommitsMentionedByReflogs: BooleanOverride.Enabled, + issueLinkingConfig: null, + lastImportAt: 0, + name: 'Custom Name', + onlyFollowFirstParent: BooleanOverride.Disabled, + onRepoLoadShowCheckedOutBranch: BooleanOverride.Enabled, + onRepoLoadShowSpecificBranches: ['master'], + pullRequestConfig: null, + cicdConfigs: cicdConfigsIn, + cicdNonce: null, + showRemoteBranches: true, + showRemoteBranchesV2: BooleanOverride.Enabled, + showStashes: BooleanOverride.Enabled, + showTags: BooleanOverride.Enabled, + workspaceFolderIndex: 0 + }; + extensionContext.workspaceState.get.mockReturnValueOnce({ + '/path/to/repo': repoState + }); + + // Run + const result = extensionState.getRepos(); + + // Assert + expect(result).toStrictEqual({ + '/path/to/repo': Object.assign({}, repoState, { + cicdConfigs: cicdConfigsOut, + cicdNonce: null + }) + }); + }); + + it('Should return the stored repositories with decrypt cicdConfigs is null', () => { + // Setup + const cicdConfigsIn: any = null; + const cicdConfigsOut: any = null; + const repoState: GitRepoState = { + cdvDivider: 0.5, + cdvHeight: 250, + columnWidths: null, + commitOrdering: RepoCommitOrdering.AuthorDate, + fileViewType: FileViewType.List, + hideRemotes: [], + includeCommitsMentionedByReflogs: BooleanOverride.Enabled, + issueLinkingConfig: null, + lastImportAt: 0, + name: 'Custom Name', + onlyFollowFirstParent: BooleanOverride.Disabled, + onRepoLoadShowCheckedOutBranch: BooleanOverride.Enabled, + onRepoLoadShowSpecificBranches: ['master'], + pullRequestConfig: null, + cicdConfigs: cicdConfigsIn, + cicdNonce: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d', + showRemoteBranches: true, + showRemoteBranchesV2: BooleanOverride.Enabled, + showStashes: BooleanOverride.Enabled, + showTags: BooleanOverride.Enabled, + workspaceFolderIndex: 0 + }; + extensionContext.workspaceState.get.mockReturnValueOnce({ + '/path/to/repo': repoState + }); + + // Run + const result = extensionState.getRepos(); + + // Assert + expect(result).toStrictEqual({ + '/path/to/repo': Object.assign({}, repoState, { + cicdConfigs: cicdConfigsOut, + cicdNonce: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d' + }) + }); + }); + + it('Should return the stored repositories with decrypt cicdUrl is undefined', () => { + // Setup + const cicdConfigsIn: any = [{ + provider: CICDProvider.GitHubV3, + cicdToken: '31613262336334643565366631613262:aedfce60882614d275f01b86aa151bcd' + }]; + const cicdConfigsOut: any = []; + const repoState: GitRepoState = { + cdvDivider: 0.5, + cdvHeight: 250, + columnWidths: null, + commitOrdering: RepoCommitOrdering.AuthorDate, + fileViewType: FileViewType.List, + hideRemotes: [], + includeCommitsMentionedByReflogs: BooleanOverride.Enabled, + issueLinkingConfig: null, + lastImportAt: 0, + name: 'Custom Name', + onlyFollowFirstParent: BooleanOverride.Disabled, + onRepoLoadShowCheckedOutBranch: BooleanOverride.Enabled, + onRepoLoadShowSpecificBranches: ['master'], + pullRequestConfig: null, + cicdConfigs: cicdConfigsIn, + cicdNonce: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d', + showRemoteBranches: true, + showRemoteBranchesV2: BooleanOverride.Enabled, + showStashes: BooleanOverride.Enabled, + showTags: BooleanOverride.Enabled, + workspaceFolderIndex: 0 + }; + extensionContext.workspaceState.get.mockReturnValueOnce({ + '/path/to/repo': repoState + }); + + // Run + const result = extensionState.getRepos(); + + // Assert + expect(result).toStrictEqual({ + '/path/to/repo': Object.assign({}, repoState, { + cicdConfigs: cicdConfigsOut, + cicdNonce: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d' + }) + }); + }); }); describe('saveRepos', () => { @@ -367,6 +572,104 @@ describe('ExtensionState', () => { // Assert expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('repoStates', repos); }); + + it('Should store the provided repositories in the workspace state with encrypt', () => { + // Setup + const cicdConfigsIn = [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: 'token' + }]; + const cicdConfigsOut = [{ + provider: CICDProvider.GitHubV3, + cicdUrl: 'https://github.com/keydepth/vscode-git-graph.git', + cicdToken: '31613262336334643565366631613262:aedfce60882614d275f01b86aa151bcd' + }]; + const repoState: GitRepoState = { + cdvDivider: 0.5, + cdvHeight: 250, + columnWidths: null, + commitOrdering: RepoCommitOrdering.AuthorDate, + fileViewType: FileViewType.List, + hideRemotes: [], + includeCommitsMentionedByReflogs: BooleanOverride.Enabled, + issueLinkingConfig: null, + lastImportAt: 0, + name: 'Custom Name', + onlyFollowFirstParent: BooleanOverride.Disabled, + onRepoLoadShowCheckedOutBranch: BooleanOverride.Enabled, + onRepoLoadShowSpecificBranches: ['master'], + pullRequestConfig: null, + cicdConfigs: cicdConfigsIn, + cicdNonce: null, + showRemoteBranches: true, + showRemoteBranchesV2: BooleanOverride.Enabled, + showStashes: BooleanOverride.Enabled, + showTags: BooleanOverride.Enabled, + workspaceFolderIndex: 0 + }; + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + jest.spyOn(utils, 'getNonce').mockReturnValueOnce('1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d'); + const buf16 = Buffer.from('1a2b3c4d5e6f1a2b'); + jest.spyOn(crypto, 'randomBytes').mockImplementation(() => buf16); + + // Run + extensionState.saveRepos({ + '/path/to/repo': repoState + }); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('repoStates', { + '/path/to/repo': Object.assign({}, repoState, { + cicdConfigs: cicdConfigsOut, + cicdNonce: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d' + }) + }); + }); + + it('Should store the provided repositories in the workspace state with encrypt cicdConfigs is null', () => { + // Setup + const repoState: GitRepoState = { + cdvDivider: 0.5, + cdvHeight: 250, + columnWidths: null, + commitOrdering: RepoCommitOrdering.AuthorDate, + fileViewType: FileViewType.List, + hideRemotes: [], + includeCommitsMentionedByReflogs: BooleanOverride.Enabled, + issueLinkingConfig: null, + lastImportAt: 0, + name: 'Custom Name', + onlyFollowFirstParent: BooleanOverride.Disabled, + onRepoLoadShowCheckedOutBranch: BooleanOverride.Enabled, + onRepoLoadShowSpecificBranches: ['master'], + pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, + showRemoteBranches: true, + showRemoteBranchesV2: BooleanOverride.Enabled, + showStashes: BooleanOverride.Enabled, + showTags: BooleanOverride.Enabled, + workspaceFolderIndex: 0 + }; + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + jest.spyOn(utils, 'getNonce').mockReturnValueOnce('1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d'); + const buf16 = Buffer.from('1a2b3c4d5e6f1a2b'); + jest.spyOn(crypto, 'randomBytes').mockImplementation(() => buf16); + + // Run + extensionState.saveRepos({ + '/path/to/repo': repoState + }); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('repoStates', { + '/path/to/repo': Object.assign({}, repoState, { + cicdConfigs: [], + cicdNonce: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d' + }) + }); + }); }); describe('transferRepo', () => { @@ -919,6 +1222,150 @@ describe('ExtensionState', () => { }); }); + describe('getCICDCache', () => { + it('Should return the stored code reviews', () => { + // Setup + const cicdCache = { + '/path/to/repo': { + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2': { + 'id000': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: 'boolean', + allow_failure: 'boolean' + } + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(cicdCache); + + // Run + const result = extensionState.getCICDCache(); + + // Assert + expect(extensionContext.workspaceState.get).toHaveBeenCalledWith('cicdCache', {}); + expect(result).toBe(cicdCache); + }); + + it('Should return the default value if not defined', () => { + // Setup + extensionContext.workspaceState.get.mockImplementationOnce((_, defaultValue) => defaultValue); + + // Run + const result = extensionState.getCICDCache(); + + // Assert + expect(extensionContext.workspaceState.get).toHaveBeenCalledWith('cicdCache', {}); + expect(result).toStrictEqual({}); + }); + }); + + + describe('saveCICD', () => { + it('Should save the cicd to the cicd unknown cache', () => { + // Setup + const cicdCache = { + '/path/to/repo': { + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2': { + 'id000': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: true + } + } + } + }; + const cicdDataSaveVal: CICDDataSave = { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: true + }; + extensionContext.workspaceState.get.mockReturnValueOnce({}); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.saveCICD('/path/to/repo', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'id000', cicdDataSaveVal); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('cicdCache', cicdCache); + }); + + it('Should save the cicd to the cicd known cache', () => { + // Setup + const cicdCache = { + '/path/to/repo': { + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2': { + 'id000': { + name: 'string', + status: 'string', + ref: 'string', + web_url: 'string', + event: 'string', + detail: true, + allow_failure: true + } + } + } + }; + const cicdDataSaveVal: CICDDataSave = { + name: 'string1', + status: 'string1', + ref: 'string1', + web_url: 'string1', + event: 'string1', + detail: false, + allow_failure: false + }; + const cicdCacheUpdated = { + '/path/to/repo': { + 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2': { + 'id000': { + name: 'string1', + status: 'string1', + ref: 'string1', + web_url: 'string1', + event: 'string1', + detail: false, + allow_failure: false + } + } + } + }; + extensionContext.workspaceState.get.mockReturnValueOnce(cicdCache); + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.saveCICD('/path/to/repo', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'id000', cicdDataSaveVal); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('cicdCache', cicdCacheUpdated); + }); + }); + + describe('clearCICDCache', () => { + it('Should clear all cicd from the cache', async () => { + // Setup + extensionContext.workspaceState.update.mockResolvedValueOnce(null); + + // Run + extensionState.clearCICDCache(); + + // Assert + expect(extensionContext.workspaceState.update).toHaveBeenCalledWith('cicdCache', {}); + }); + }); + describe('startCodeReview', () => { it('Should store the code review (in a repository with no prior code reviews)', async () => { // Setup diff --git a/tests/gitGraphView.test.ts b/tests/gitGraphView.test.ts index 81119abc..b8038ecd 100644 --- a/tests/gitGraphView.test.ts +++ b/tests/gitGraphView.test.ts @@ -9,6 +9,7 @@ jest.mock('../src/repoManager'); import * as path from 'path'; import { ConfigurationChangeEvent } from 'vscode'; import { AvatarEvent, AvatarManager } from '../src/avatarManager'; +import { CICDEvent, CicdManager } from '../src/cicdManager'; import { DataSource } from '../src/dataSource'; import { ExtensionState } from '../src/extensionState'; import { GitGraphView, standardiseCspSource } from '../src/gitGraphView'; @@ -26,11 +27,13 @@ describe('GitGraphView', () => { let onDidChangeGitExecutable: EventEmitter; let onDidChangeRepos: EventEmitter; let onAvatar: EventEmitter; + let onCICD: EventEmitter; let logger: Logger; let dataSource: DataSource; let extensionState: ExtensionState; let avatarManager: AvatarManager; + let cicdManager: CicdManager; let repoManager: RepoManager; let spyOnLog: jest.SpyInstance; @@ -43,12 +46,15 @@ describe('GitGraphView', () => { onDidChangeGitExecutable = new EventEmitter(); onDidChangeRepos = new EventEmitter(); onAvatar = new EventEmitter(); + onCICD = new EventEmitter(); logger = new Logger(); dataSource = new DataSource({ path: '/path/to/git', version: '2.25.0' }, onDidChangeConfiguration.subscribe, onDidChangeGitExecutable.subscribe, logger); extensionState = new ExtensionState(vscode.mocks.extensionContext, onDidChangeGitExecutable.subscribe); + jest.spyOn(extensionState, 'getCICDCache').mockReturnValue({}); avatarManager = new AvatarManager(dataSource, extensionState, logger); repoManager = new RepoManager(dataSource, extensionState, onDidChangeConfiguration.subscribe, logger); + cicdManager = new CicdManager(extensionState, repoManager, logger); spyOnLog = jest.spyOn(logger, 'log'); spyOnLogError = jest.spyOn(logger, 'logError'); @@ -63,16 +69,21 @@ describe('GitGraphView', () => { Object.defineProperty(avatarManager, 'onAvatar', { get: () => onAvatar.subscribe }); + Object.defineProperty(cicdManager, 'onCICD', { + get: () => onCICD.subscribe + }); jest.spyOn(extensionState, 'getLastActiveRepo').mockReturnValue(null); }); afterAll(() => { repoManager.dispose(); avatarManager.dispose(); + cicdManager.dispose(); extensionState.dispose(); dataSource.dispose(); logger.dispose(); onAvatar.dispose(); + onCICD.dispose(); onDidChangeRepos.dispose(); onDidChangeGitExecutable.dispose(); onDidChangeConfiguration.dispose(); @@ -95,7 +106,7 @@ describe('GitGraphView', () => { }; // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert expect(vscode.window.createWebviewPanel).toHaveBeenCalledWith('git-graph', 'Git Graph', vscode.ViewColumn.Two, { @@ -108,10 +119,10 @@ describe('GitGraphView', () => { it('Should reveal the existing WebviewPanel (when one exists)', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -121,12 +132,12 @@ describe('GitGraphView', () => { it('Should reveal the existing WebviewPanel (when one exists, but it isn\'t visible)', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo' }); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo' }); const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); mockedWebviewPanel.mocks.panel.setVisibility(false); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo' }); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo' }); // Assert expect(vscode.window.createWebviewPanel).toHaveBeenCalledTimes(1); @@ -136,10 +147,10 @@ describe('GitGraphView', () => { it('Should reveal the existing WebviewPanel (when one exists), and send loadViewTo', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo' }); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo' }); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -159,7 +170,7 @@ describe('GitGraphView', () => { it('Should construct a new WebviewPanel, providing loadViewTo', () => { // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, { repo: '/path/to/repo' }); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, { repo: '/path/to/repo' }); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -172,7 +183,7 @@ describe('GitGraphView', () => { vscode.window.activeTextEditor = undefined; // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert expect(vscode.window.createWebviewPanel).toHaveBeenCalledWith('git-graph', 'Git Graph', vscode.ViewColumn.One, { @@ -188,7 +199,7 @@ describe('GitGraphView', () => { vscode.mockExtensionSettingReturnValue('retainContextWhenHidden', true); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert expect(vscode.window.createWebviewPanel).toHaveBeenCalledWith('git-graph', 'Git Graph', vscode.ViewColumn.One, { @@ -204,7 +215,7 @@ describe('GitGraphView', () => { vscode.mockExtensionSettingReturnValue('retainContextWhenHidden', false); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert expect(vscode.window.createWebviewPanel).toHaveBeenCalledWith('git-graph', 'Git Graph', vscode.ViewColumn.One, { @@ -220,7 +231,7 @@ describe('GitGraphView', () => { vscode.mockExtensionSettingReturnValue('tabIconColourTheme', 'colour'); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -232,7 +243,7 @@ describe('GitGraphView', () => { vscode.mockExtensionSettingReturnValue('tabIconColourTheme', 'grey'); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -245,7 +256,7 @@ describe('GitGraphView', () => { describe('WebviewPanel.onDidDispose', () => { it('Should dispose the GitGraphView when the WebviewPanel is disposed', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Run const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -259,7 +270,7 @@ describe('GitGraphView', () => { describe('WebviewPanel.onDidChangeViewState', () => { it('Should transition from visible to not-visible correctly', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); const spyOnRepoFileWatcherStop = jest.spyOn(GitGraphView.currentPanel!['repoFileWatcher'], 'stop'); @@ -274,7 +285,7 @@ describe('GitGraphView', () => { it('Should transition from not-visible to visible correctly', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); mockedWebviewPanel.mocks.panel.setVisibility(false); GitGraphView.currentPanel!['panel']['webview'].html = ''; @@ -289,7 +300,7 @@ describe('GitGraphView', () => { it('Should ignore events if they have no effect', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); GitGraphView.currentPanel!['panel']['webview'].html = ''; @@ -305,7 +316,7 @@ describe('GitGraphView', () => { describe('RepoManager.onDidChangeRepos', () => { it('Should send the updated repositories to the front-end when the view is already loaded', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Run onDidChangeRepos.emit({ @@ -330,7 +341,7 @@ describe('GitGraphView', () => { it('Should send the updated repositories to the front-end when the view is already loaded (with loadViewTo)', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Run onDidChangeRepos.emit({ @@ -357,7 +368,7 @@ describe('GitGraphView', () => { it('Shouldn\'t send the updated repositories to the front-end when the view is not visible', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); mockedWebviewPanel.mocks.panel.setVisibility(false); @@ -374,7 +385,7 @@ describe('GitGraphView', () => { it('Should transition to no repositories correctly', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); spyOnGetRepos.mockReturnValueOnce({}); // Run @@ -393,7 +404,7 @@ describe('GitGraphView', () => { it('Should transition from no repositories correctly', () => { // Setup spyOnGetRepos.mockReturnValueOnce({}); - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Run onDidChangeRepos.emit({ @@ -414,7 +425,7 @@ describe('GitGraphView', () => { describe('AvatarManager.onAvatar', () => { it('Should send the avatar', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Run onAvatar.emit({ @@ -434,10 +445,35 @@ describe('GitGraphView', () => { }); }); + describe('CicdManager.onCICD', () => { + it('Should send the cicd', () => { + // Setup + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); + + // Run + onCICD.emit({ + repo: 'path/to/repo', + hash: 'data:image/png;base64,YmluYXJ5LWltYWdlLWRhdGE=', + cicdDataSaves: {} + }); + + // Assert + const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); + expect(mockedWebviewPanel.mocks.messages).toStrictEqual([ + { + command: 'fetchCICD', + repo: 'path/to/repo', + hash: 'data:image/png;base64,YmluYXJ5LWltYWdlLWRhdGE=', + cicdDataSaves: {} + } + ]); + }); + }); + describe('RepoFileWatcher.repoChangeCallback', () => { it('Should refresh the view when it\'s visible', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Run GitGraphView.currentPanel!['repoFileWatcher']['repoChangeCallback'](); @@ -453,7 +489,7 @@ describe('GitGraphView', () => { it('Shouldn\'t refresh the view when it isn\'t visible', () => { // Setup - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); mockedWebviewPanel.mocks.panel.setVisibility(false); @@ -473,7 +509,7 @@ describe('GitGraphView', () => { let spyOnRepoFileWatcherMute: jest.SpyInstance; let spyOnRepoFileWatcherUnmute: jest.SpyInstance; beforeEach(() => { - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); onDidReceiveMessage = mockedWebviewPanel.mocks.panel.webview.onDidReceiveMessage; @@ -977,7 +1013,8 @@ describe('GitGraphView', () => { avatar: getAvatarImageResolvedValue, codeReview: getCodeReviewResolvedValue, refresh: false, - error: null + error: null, + cicdDataSaves: {} } ]); }); @@ -1014,7 +1051,8 @@ describe('GitGraphView', () => { avatar: null, codeReview: null, refresh: false, - error: null + error: null, + cicdDataSaves: {} } ]); }); @@ -1058,7 +1096,8 @@ describe('GitGraphView', () => { avatar: null, codeReview: getCodeReviewResolvedValue, refresh: false, - error: null + error: null, + cicdDataSaves: {} } ]); }); @@ -3472,7 +3511,7 @@ describe('GitGraphView', () => { describe('sendMessage', () => { beforeEach(() => { - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); spyOnLog.mockReset(); spyOnLogError.mockReset(); }); @@ -3585,7 +3624,7 @@ describe('GitGraphView', () => { spyOnIsGitExecutableUnknown.mockReturnValueOnce(true); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -3598,7 +3637,7 @@ describe('GitGraphView', () => { spyOnGetRepos.mockResolvedValueOnce({}); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -3613,7 +3652,7 @@ describe('GitGraphView', () => { vscode.mockExtensionSettingReturnValue('repository.commits.fetchAvatars', false); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); @@ -3629,7 +3668,7 @@ describe('GitGraphView', () => { vscode.mockExtensionSettingReturnValue('repository.commits.fetchAvatars', true); // Run - GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, repoManager, logger, null); + GitGraphView.createOrShow('/path/to/extension', dataSource, extensionState, avatarManager, cicdManager, repoManager, logger, null); // Assert const mockedWebviewPanel = vscode.getMockedWebviewPanel(0); diff --git a/tests/repoManager.test.ts b/tests/repoManager.test.ts index eedac555..a3ed1ca1 100644 --- a/tests/repoManager.test.ts +++ b/tests/repoManager.test.ts @@ -1237,6 +1237,8 @@ describe('RepoManager', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, @@ -1906,6 +1908,8 @@ describe('RepoManager', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, @@ -2259,6 +2263,8 @@ describe('RepoManager', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, @@ -2337,6 +2343,8 @@ describe('RepoManager', () => { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + cicdConfigs: null, + cicdNonce: null, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, diff --git a/web/dialog.ts b/web/dialog.ts index 92d762d5..dd084afe 100644 --- a/web/dialog.ts +++ b/web/dialog.ts @@ -11,6 +11,7 @@ const enum DialogType { const enum DialogInputType { Text, TextRef, + Password, Select, Radio, Checkbox @@ -31,6 +32,13 @@ interface DialogTextRefInput { readonly info?: string; } +interface DialogPasswordInput { + readonly type: DialogInputType.Password; + readonly name: string; + readonly default: string; + readonly info?: string; +} + type DialogSelectInput = { readonly type: DialogInputType.Select; readonly name: string; @@ -71,7 +79,7 @@ interface DialogRadioInputOption { readonly value: string; } -type DialogInput = DialogTextInput | DialogTextRefInput | DialogSelectInput | DialogRadioInput | DialogCheckboxInput; +type DialogInput = DialogTextInput | DialogTextRefInput | DialogPasswordInput | DialogSelectInput | DialogRadioInput | DialogCheckboxInput; type DialogInputValue = string | string[] | boolean; type DialogTarget = { @@ -212,6 +220,8 @@ class Dialog { inputHtml = '
' + (infoColRequired ? '' + infoHtml + '' : ''); } else if (input.type === DialogInputType.Checkbox) { inputHtml = ''; + } else if (input.type === DialogInputType.Password) { + inputHtml = '' + (infoColRequired ? '' + infoHtml + '' : ''); } else { inputHtml = '' + (infoColRequired ? '' + infoHtml + '' : ''); } diff --git a/web/global.d.ts b/web/global.d.ts index 5e1072ff..c7aa9694 100644 --- a/web/global.d.ts +++ b/web/global.d.ts @@ -20,6 +20,7 @@ declare global { const workspaceState: GG.DeepReadonly; type AvatarImageCollection = { [email: string]: string }; + type CICDDataCollection = { [repo: string]: { [hash: string]: { [id: string]: GG.CICDDataSave } } }; interface ExpandedCommit { index: number; @@ -57,6 +58,7 @@ declare global { readonly commits: GG.GitCommit[]; readonly commitHead: string | null; readonly avatars: AvatarImageCollection; + readonly cicdDatas: CICDDataCollection; readonly currentBranches: string[] | null; readonly moreCommitsAvailable: boolean; readonly maxCommits: number; diff --git a/web/main.ts b/web/main.ts index b12ba1e5..2ea5234b 100644 --- a/web/main.ts +++ b/web/main.ts @@ -11,6 +11,7 @@ class GitGraphView { private commitLookup: { [hash: string]: number } = {}; private onlyFollowFirstParent: boolean = false; private avatars: AvatarImageCollection = {}; + private cicdDatas: CICDDataCollection = {}; private currentBranches: string[] | null = null; private currentRepo!: string; @@ -124,6 +125,7 @@ class GitGraphView { this.maxCommits = prevState.maxCommits; this.expandedCommit = prevState.expandedCommit; this.avatars = prevState.avatars; + this.cicdDatas = prevState.cicdDatas; this.gitConfig = prevState.gitConfig; this.loadRepoInfo(prevState.gitBranches, prevState.gitBranchHead, prevState.gitRemotes, prevState.gitStashes, true); this.loadCommits(prevState.commits, prevState.commitHead, prevState.gitTags, prevState.moreCommitsAvailable, prevState.onlyFollowFirstParent); @@ -517,6 +519,54 @@ class GitGraphView { } } + public loadCicd(repo: string, hash: string, cicdDataSaves: { [id: string]: GG.CICDDataSave }) { + if (typeof this.cicdDatas[repo] === 'undefined') { + this.cicdDatas[repo] = {}; + } + this.cicdDatas[repo][hash] = cicdDataSaves; + this.saveState(); + let cicdDetailElems = >document.getElementsByClassName('cicdDetail'); + for (let i = 0; i < cicdDetailElems.length; i++) { + if (cicdDetailElems[i].dataset.hash === hash) { + cicdDetailElems[i].innerHTML = this.getCicdHtml(cicdDataSaves, true); + } + } + } + + private getCicdHtml(cicdDataSaves: { [id: string]: GG.CICDDataSave }, detail: boolean = false) { + let ret: string = ''; + for (let name in cicdDataSaves) { + // Multiplicity + // GitHub: Commit 1 - 0..n Workflow 0..n - 1 PullRequest 0..1 - 1..n Commit + // GitLab: Commit 1 - 0..n Pipeline 0..n - 1 MergeRequest 0..1 - 1..n Commit + let cicdDataSave = cicdDataSaves[name]; + let event = cicdDataSave.event || ''; + let detailCurrent = cicdDataSave.detail || false; + let status = ((cicdDataSave.status === 'success' || cicdDataSave.status === 'SUCCESS') ? 'G' : + (((typeof cicdDataSave.allow_failure === 'undefined' || !cicdDataSave.allow_failure) && + (cicdDataSave.status === 'failed' || cicdDataSave.status === 'failure')) ? 'B' : + ((typeof cicdDataSave.allow_failure !== 'undefined' || cicdDataSave.allow_failure) && + (cicdDataSave.status === 'failed' || cicdDataSave.status === 'failure')) ? 'A' : 'U')); + // if (detailCurrent === detail && event === 'push' || event === 'pull_request' || event === '') { + if (detailCurrent === detail && event !== 'issues' && event !== 'issue_comment' && event !== 'schedule' && event !== 'workflow_run') { + ret += + '' + + // '
' + + `
` + + (typeof cicdDataSave.name !== 'undefined' ? `
${cicdDataSave.name}
` : '') + + (typeof cicdDataSave.status !== 'undefined' ? `
Status: ${cicdDataSave.status}
` : '') + + ((typeof cicdDataSave.event !== 'undefined' && cicdDataSave.event !== '') ? `
Event: ${cicdDataSave.event}
` : '') + + ((typeof cicdDataSave.allow_failure !== 'undefined' && cicdDataSave.allow_failure) ? `
Allow Failure: ${cicdDataSave.allow_failure}
` : '') + + '
' + + `${(status === 'G' ? SVG_ICONS.passed : (status === 'B' ? SVG_ICONS.failed : (status === 'A' ? SVG_ICONS.alert : SVG_ICONS.inconclusive)))}` + + '
'; + } + } + if (ret === '') { + ret = '-'; + } + return ret; + } /* Getters */ @@ -721,6 +771,7 @@ class GitGraphView { commits: this.commits, commitHead: this.commitHead, avatars: this.avatars, + cicdDatas: this.cicdDatas, currentBranches: this.currentBranches, moreCommitsAvailable: this.moreCommitsAvailable, maxCommits: this.maxCommits, @@ -2189,6 +2240,8 @@ class GitGraphView { contextMenu.close(); } + } if ((eventElem = eventTarget.closest('.cicdAnchor')) !== null) { + // .cicdAnchor was clicked } else if ((eventElem = eventTarget.closest('.commit')) !== null) { // .commit was clicked if (this.expandedCommit !== null) { @@ -2540,7 +2593,11 @@ class GitGraphView { + '' + (commitDetails.authorDate !== commitDetails.committerDate ? 'Committer ' : '') + 'Date: ' + formatLongDate(commitDetails.committerDate) + '' + (expandedCommit.avatar !== null ? '' : '') - + '

' + textFormatter.format(commitDetails.body); + + '
' + + ('CI/CD detail: ' + '' + + ((typeof this.cicdDatas[this.currentRepo] === 'object' && typeof this.cicdDatas[this.currentRepo][commitDetails.hash] === 'object') ? this.getCicdHtml(this.cicdDatas[this.currentRepo][commitDetails.hash], true) : '*') + + '') + + '

' + textFormatter.format(commitDetails.body); } else { html += 'Displaying all uncommitted changes.'; } @@ -3241,6 +3298,9 @@ window.addEventListener('load', () => { gitGraph.loadAvatar(msg.email, resizedImage); }); break; + case 'fetchCICD': + gitGraph.loadCicd(msg.repo, msg.hash, msg.cicdDataSaves); + break; case 'fetchIntoLocalBranch': refreshOrDisplayError(msg.error, 'Unable to Fetch into Local Branch'); break; diff --git a/web/settingsWidget.ts b/web/settingsWidget.ts index 25d8ccea..7233f638 100644 --- a/web/settingsWidget.ts +++ b/web/settingsWidget.ts @@ -239,6 +239,28 @@ class SettingsWidget { html += '
'; } + if (this.config !== null) { + html += '

CI/CD Status Configuration

'; + const cicdConfigs = this.repo.cicdConfigs; + if (cicdConfigs !== null && cicdConfigs.length !== 0) { + cicdConfigs.forEach((cicdConfig, i) => { + let providerOptions:any = {}; + providerOptions[(GG.CICDProvider.GitHubV3).toString()] = 'GitHub'; + providerOptions[(GG.CICDProvider.GitLabV4).toString()] = 'GitLab API v4(ver8.11-)'; + providerOptions[(GG.CICDProvider.JenkinsV2).toString()] = 'Jenkins v2'; + const cicdUrl = escapeHtml(cicdConfig.cicdUrl || 'Not Set'); + html += '' + + '' + + '' + + '' + + ''; + }); + } else { + html += ''; + } + html += '
ProviderURLAction
' + escapeHtml(providerOptions[cicdConfig.provider]) + '' + cicdUrl + '
' + SVG_ICONS.pencil + '
' + SVG_ICONS.close + '
There are no CI/CD configured for this repository.
' + SVG_ICONS.plus + 'Add CI/CD
'; + } + html += '

Git Graph Configuration

' + '
' + SVG_ICONS.gear + 'Open Git Graph Extension Settings

' + '
' + SVG_ICONS.package + 'Export Repository Configuration
' + @@ -450,6 +472,85 @@ class SettingsWidget { this.view.saveRepoStateValue(this.currentRepo, 'hideRemotes', this.repo.hideRemotes); this.view.refresh(true); }); + const updateConfigWithFormValues = (values: DialogInputValue[]) => { + let config: GG.CICDConfig = { + provider: parseInt(values[0]), cicdUrl: values[1], + cicdToken: values[2] + }; + return config; + }; + const copyConfigs = () => { + if (this.repo === null) return []; + let configs: GG.CICDConfig[]; + if (this.repo.cicdConfigs === null) { + configs = []; + } else { + configs = Object.assign([], this.repo.cicdConfigs); + } + return configs; + }; + + document.getElementById('settingsAddCICD')!.addEventListener('click', () => { + let defaultProvider = GG.CICDProvider.GitHubV3.toString(); + let providerOptions = [ + // { name: 'Bitbucket', value: (GG.CICDProvider.Bitbucket).toString() }, + { name: 'GitHub', value: (GG.CICDProvider.GitHubV3).toString() }, + { name: 'GitLab API v4(ver8.11-)', value: (GG.CICDProvider.GitLabV4).toString() }, + { name: 'Jenkins v2', value: (GG.CICDProvider.JenkinsV2).toString() } + ]; + dialog.showForm('Add a new cicd to this repository:', [ + { + type: DialogInputType.Select, name: 'Provider', + options: providerOptions, default: defaultProvider, + info: 'In addition to the built-in publicly hosted CI/CD providers.' + }, + { type: DialogInputType.Text, name: 'Git/Jenkins URL', default: '', placeholder: null, info: 'The CI/CD provider\'s Git URL (e.g. https://gitlab.com/OWNER/REPO.git) / Jenkins Job URL.' }, + { type: DialogInputType.Password, name: 'Access Token', default: '', info: 'The GitHub/GitLab personal or project access token / The Jenkin user_name:password or user_name:access_token' } + ], 'Add CI/CD', (values) => { + let configs: GG.CICDConfig[] = copyConfigs(); + let config: GG.CICDConfig = updateConfigWithFormValues(values); + configs.push(config); + this.setCICDConfig(configs); + }, { type: TargetType.Repo }); + }); + + addListenerToClass('editCICD', 'click', (e) => { + const cicdConfig = this.getCICDForBtnEvent(e); + if (cicdConfig === null) return; + let providerOptions = [ + // { name: 'Bitbucket', value: (GG.CICDProvider.Bitbucket).toString() }, + { name: 'GitHub', value: (GG.CICDProvider.GitHubV3).toString() }, + { name: 'GitLab API v4(ver8.11-)', value: (GG.CICDProvider.GitLabV4).toString() }, + { name: 'Jenkins v2', value: (GG.CICDProvider.JenkinsV2).toString() } + ]; + dialog.showForm('Edit the CI/CD ' + escapeHtml(cicdConfig.cicdUrl || 'Not Set') + ':', [ + { + type: DialogInputType.Select, name: 'Provider', + options: providerOptions, default: cicdConfig.provider.toString(), + info: 'In addition to the built-in publicly hosted CI/CD providers.' + }, + { type: DialogInputType.Text, name: 'Git/Jenkins URL', default: cicdConfig.cicdUrl || '', placeholder: null, info: 'The CI/CD provider\'s Git URL (e.g. https://gitlab.com/OWNER/REPO.git) / Jenkins Job URL.' }, + { type: DialogInputType.Password, name: 'Access Token', default: cicdConfig.cicdToken, info: 'The GitHub/GitLab personal or project access token / The Jenkin user_name:password or user_name:access_token' } + ], 'Save Changes', (values) => { + let index = parseInt(((e.target).closest('.cicdBtns')!).dataset.index!); + let configs: GG.CICDConfig[] = copyConfigs(); + let config: GG.CICDConfig = updateConfigWithFormValues(values); + configs[index] = config; + this.setCICDConfig(configs); + }, { type: TargetType.Repo }); + }); + + addListenerToClass('deleteCICD', 'click', (e) => { + const cicdConfig = this.getCICDForBtnEvent(e); + if (cicdConfig === null) return; + dialog.showConfirmation('Are you sure you want to delete the CI/CD ' + escapeHtml(cicdConfig.cicdUrl) + '?', 'Yes, delete', () => { + let index = parseInt(((e.target).closest('.cicdBtns')!).dataset.index!); + let configs: GG.CICDConfig[] = copyConfigs(); + configs.splice(index, 1); + this.setCICDConfig(configs); + }, { type: TargetType.Repo }); + }); + } document.getElementById('editIssueLinking')!.addEventListener('click', () => { @@ -570,6 +671,16 @@ class SettingsWidget { this.render(); } + /** + * Save the pull request configuration for this repository. + * @param config The pull request configuration to save. + */ + private setCICDConfig(config: GG.CICDConfig[] | null) { + if (this.currentRepo === null) return; + this.view.saveRepoStateValue(this.currentRepo, 'cicdConfigs', config); + this.render(); + } + /** * Show the dialog allowing the user to configure the issue linking for this repository. * @param defaultIssueRegex The default regular expression used to match issue numbers. @@ -800,6 +911,17 @@ class SettingsWidget { }); } + /** + * Get the cicd details corresponding to a mouse event. + * @param e The mouse event. + * @returns The details of the cicd. + */ + private getCICDForBtnEvent(e: Event) { + return this.repo !== null && this.repo.cicdConfigs !== null + ? this.repo.cicdConfigs[parseInt(((e.target).closest('.cicdBtns')!).dataset.index!)] + : null; + } + /** * Get the remote details corresponding to a mouse event. * @param e The mouse event. diff --git a/web/styles/dialog.css b/web/styles/dialog.css index 96906f2d..e50b6029 100644 --- a/web/styles/dialog.css +++ b/web/styles/dialog.css @@ -83,7 +83,7 @@ body.vscode-high-contrast .dialog{ background-color:var(--vscode-menu-foreground); } -.dialogContent > table.dialogForm input[type=text]{ +.dialogContent > table.dialogForm input[type=text], .dialogContent > table.dialogForm input[type=password]{ width:100%; padding:4px 6px; box-sizing:border-box; @@ -96,10 +96,10 @@ body.vscode-high-contrast .dialog{ font-size:13px; line-height:17px; } -.dialogContent > table.dialogForm input[type=text]:focus, .dialogContent .dialogFormCheckbox > label > input[type=checkbox]:focus ~ .customCheckbox, .dialogContent .dialogFormRadio > label > input[type=radio]:focus ~ .customRadio{ +.dialogContent > table.dialogForm input[type=text]:focus, .dialogContent > table.dialogForm input[type=password]:focus, .dialogContent .dialogFormCheckbox > label > input[type=checkbox]:focus ~ .customCheckbox, .dialogContent .dialogFormRadio > label > input[type=radio]:focus ~ .customRadio{ border-color:var(--vscode-focusBorder); } -.dialogContent > table.dialogForm input[type=text]::placeholder{ +.dialogContent > table.dialogForm input[type=text]::placeholder, .dialogContent > table.dialogForm input[type=password]::placeholder{ color:var(--vscode-menu-foreground); opacity:0.4; } diff --git a/web/styles/main.css b/web/styles/main.css index 9e9a532f..6dec24a0 100644 --- a/web/styles/main.css +++ b/web/styles/main.css @@ -456,6 +456,28 @@ code{ opacity:0.8; } +.cicdInfo{ + vertical-align:top; + display:inline-block; + width:13px; + height:13px; + vertical-align:top; + margin-top:3px; + margin-right:4px; +} +.cicdInfo.G svg{ + fill:#009028; + opacity:0.9; +} +.cicdInfo.U svg, .cicdInfo.A svg{ + fill:#f09000; + opacity:1; +} +.cicdInfo.B svg{ + fill:#e00000; + opacity:0.8; +} + #cdvFiles{ left:50%; right:0; @@ -1020,4 +1042,78 @@ label > input[type=radio]:checked ~ .customRadio:after{ #commitGraph, #commitTable th, #commitTable td, .gitRef, #loadingHeader, .unselectable, .roundedBtn, #controls label{ -webkit-user-select:none; user-select:none; -} \ No newline at end of file +} + +/* cicdTooltip */ + +.cicdTooltip{ + display: inline-block; + position: relative; + cursor: pointer; +} +.cicdTooltip .cicdTooltipContent{ + position: absolute; + border-style: solid; + /* transform: translateY(-50%); */ + visibility: hidden; + z-index: 1; +} +/* .cicdTooltip .cicdTooltipContent::before{ + position: absolute; + border-style: solid; + transform: translateY(-50%); + content: ""; + height: 0; + width: 0; + top: 50%; +} */ +.cicdTooltip .cicdTooltipContent{ + background-color:var(--vscode-menu-background); + border-radius: 5px; + border-width: 2px; + color:var(--vscode-menu-foreground); + display: block; + font-size: 13px; + font-weight: 600; + /* top: 50%; */ + top: calc(100%); + white-space: nowrap; +} +.cicdTooltip .cicdTooltipContent.G{ + border-color: #009028; +} +.cicdTooltip .cicdTooltipContent.U, .cicdTooltip .cicdTooltipContent.A{ + border-color: #f09000; +} +.cicdTooltip .cicdTooltipContent.B{ + border-color: #e00000; +} +/* .cicdTooltip .cicdTooltipContent.right::before{ + border-color: transparent rgba(128,128,128,1.0) transparent transparent; + border-width: 5px 9px 5px 0; + right: 100%; +} */ +.cicdTooltip .cicdTooltipContent.right{ + left: calc(100% + 13px); +} +/* .cicdTooltip .cicdTooltipContent.left::before{ + border-color: transparent transparent transparent rgba(128,128,128,1.0); + border-width: 5px 0 5px 9px; + left: 100%; +} */ +.cicdTooltip .cicdTooltipContent.left{ + right: calc(100% + 13px); +} +.cicdTooltip:hover .cicdTooltipContent{ + visibility: visible; +} +.cicdTooltipTitle, .cicdTooltipSection{ + padding:3px 10px; +} +.cicdTooltipTitle{ + text-align:center; + font-weight:700; +} +.cicdTooltipSection{ + border-top:1px solid rgba(128,128,128,0.5); +} diff --git a/web/styles/settingsWidget.css b/web/styles/settingsWidget.css index cc26941b..f713074d 100644 --- a/web/styles/settingsWidget.css +++ b/web/styles/settingsWidget.css @@ -140,17 +140,17 @@ vertical-align:top; cursor:pointer; } -.settingsSection > table td.btns.remoteBtns div{ +.settingsSection > table td.btns.remoteBtns div, .settingsSection > table td.btns.cicdBtns div{ vertical-align:middle; } -.settingsSection > table #editRepoName svg, .settingsSection > table #editInitialBranches svg, .settingsSection > table .editRemote svg{ +.settingsSection > table #editRepoName svg, .settingsSection > table #editInitialBranches svg, .settingsSection > table .editRemote svg, .settingsSection > table .editCICD svg{ position:absolute; left:1.5px; top:1.5px; width:14px !important; height:14px !important; } -.settingsSection > table #deleteRepoName svg, .settingsSection > table #clearInitialBranches svg, .settingsSection > table .deleteRemote svg, .settingsSection > table .fetchRemote svg, .settingsSection > table .pruneRemote svg{ +.settingsSection > table #deleteRepoName svg, .settingsSection > table #clearInitialBranches svg, .settingsSection > table .deleteRemote svg, .settingsSection > table .fetchRemote svg, .settingsSection > table .pruneRemote svg, .settingsSection > table .deleteCICD svg{ position:absolute; left:0.5px; top:0.5px;