const fs = require('fs-extra'); const fetch = require('node-fetch'); const path = require('path'); const { updateConfig } = require('../utils/config'); const { escapeRegExp } = require('../utils/regexp'); const { getExpectationsFilename, transformRecordedData: transformData, getGitClient, } = require('./common'); const { merge } = require('lodash'); const BITBUCKET_REPO_OWNER_SANITIZED_VALUE = 'owner'; const BITBUCKET_REPO_NAME_SANITIZED_VALUE = 'repo'; const BITBUCKET_REPO_TOKEN_SANITIZED_VALUE = 'fakeToken'; const FAKE_OWNER_USER = { name: 'owner', display_name: 'owner', links: { avatar: { href: 'https://avatars1.githubusercontent.com/u/7892489?v=4', }, }, nickname: 'owner', }; async function getEnvs() { const { BITBUCKET_REPO_OWNER: owner, BITBUCKET_REPO_NAME: repo, BITBUCKET_OUATH_CONSUMER_KEY: consumerKey, BITBUCKET_OUATH_CONSUMER_SECRET: consumerSecret, } = process.env; if (!owner || !repo || !consumerKey || !consumerSecret) { throw new Error( 'Please set BITBUCKET_REPO_OWNER, BITBUCKET_REPO_NAME, BITBUCKET_OUATH_CONSUMER_KEY, BITBUCKET_OUATH_CONSUMER_SECRET environment variables', ); } const params = new URLSearchParams(); params.append('grant_type', 'client_credentials'); const { access_token: token } = await fetch( `https://${consumerKey}:${consumerSecret}@bitbucket.org/site/oauth2/access_token`, { method: 'POST', body: params }, ).then(r => r.json()); return { owner, repo, token }; } const API_URL = 'https://api.bitbucket.org/2.0/'; function get(token, path) { return fetch(`${API_URL}${path}`, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }).then(r => r.json()); } function post(token, path, body) { return fetch(`${API_URL}${path}`, { method: 'POST', ...(body ? { body } : {}), headers: { 'Content-Type': 'application/json', ...(body ? { 'Content-Type': 'application/json' } : {}), Authorization: `Bearer ${token}`, }, }); } function del(token, path) { return fetch(`${API_URL}${path}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); } async function prepareTestBitBucketRepo({ lfs }) { const { owner, repo, token } = await getEnvs(); // postfix a random string to avoid collisions const postfix = Math.random().toString(32).slice(2); const testRepoName = `${repo}-${Date.now()}-${postfix}`; console.info('Creating repository', testRepoName, token); const response = await post( token, `repositories/${owner}/${testRepoName}`, JSON.stringify({ scm: 'git' }), ); if (!response.ok) { throw new Error(`Unable to create repository. ${response.statusText}`); } const tempDir = path.join('.temp', testRepoName); await fs.remove(tempDir); let git = getGitClient(); const repoUrl = `git@bitbucket.org:${owner}/${repo}.git`; console.info('Cloning repository', repoUrl); await git.clone(repoUrl, tempDir); git = getGitClient(tempDir); console.info('Pushing to new repository', testRepoName); console.info('Updating remote...'); await git.removeRemote('origin'); await git.addRemote('origin', `git@bitbucket.org:${owner}/${testRepoName}`); console.info('Pushing...'); await git.push(['-u', 'origin', 'main']); console.info('Pushed to new repository', testRepoName); if (lfs) { console.info(`Enabling LFS for repo ${owner}/${repo}`); await git.addConfig('commit.gpgsign', 'false'); await git.raw(['lfs', 'track', '*.png', '*.jpg']); await git.add('.gitattributes'); await git.commit('chore: track images files under LFS'); await git.push('origin', 'main'); } return { owner, repo: testRepoName, tempDir }; } async function getUser() { const { token } = await getEnvs(); const user = await get(token, 'user'); return { ...user, token, backendName: 'bitbucket' }; } async function deleteRepositories({ owner, repo, tempDir }) { const { token } = await getEnvs(); console.info('Deleting repository', `${owner}/${repo}`); await fs.remove(tempDir); await del(token, `repositories/${owner}/${repo}`); } async function resetOriginRepo({ owner, repo, tempDir }) { console.info('Resetting origin repo:', `${owner}/${repo}`); const { token } = await getEnvs(); const pullRequests = await get(token, `repositories/${owner}/${repo}/pullrequests`); const ids = pullRequests.values.map(mr => mr.id); console.info('Closing pull requests:', ids); await Promise.all( ids.map(id => post(token, `repositories/${owner}/${repo}/pullrequests/${id}/decline`)), ); const branches = await get(token, `repositories/${owner}/${repo}/refs/branches`); const toDelete = branches.values.filter(b => b.name !== 'main').map(b => b.name); console.info('Deleting branches', toDelete); await Promise.all( toDelete.map(branch => del(token, `repositories/${owner}/${repo}/refs/branches/${branch}`)), ); console.info('Resetting main'); const git = getGitClient(tempDir); await git.push(['--force', 'origin', 'main']); console.info('Done resetting origin repo:', `${owner}/${repo}`); } async function resetRepositories({ owner, repo, tempDir }) { await resetOriginRepo({ owner, repo, tempDir }); } async function setupBitBucket(options) { const { lfs = false, ...rest } = options; console.info('Running tests - live data will be used!'); const [user, repoData] = await Promise.all([getUser(), prepareTestBitBucketRepo({ lfs })]); console.info('Updating config...'); await updateConfig(config => { merge(config, rest, { backend: { repo: `${repoData.owner}/${repoData.repo}`, }, }); }); return { ...repoData, user }; } async function teardownBitBucket(taskData) { await deleteRepositories(taskData); return null; } async function setupBitBucketTest(taskData) { await resetRepositories(taskData); return null; } const sanitizeString = (str, { owner, repo, token, ownerName }) => { let replaced = str .replace(new RegExp(escapeRegExp(owner), 'g'), BITBUCKET_REPO_OWNER_SANITIZED_VALUE) .replace(new RegExp(escapeRegExp(repo), 'g'), BITBUCKET_REPO_NAME_SANITIZED_VALUE) .replace(new RegExp(escapeRegExp(token), 'g'), BITBUCKET_REPO_TOKEN_SANITIZED_VALUE) .replace( new RegExp('https://secure.gravatar.+?/u/.+?v=\\d', 'g'), `${FAKE_OWNER_USER.links.avatar.href}`, ) .replace(new RegExp(/\?token=.+?&/g), 'token=fakeToken&') .replace(new RegExp(/&client=.+?&/g), 'client=fakeClient&'); if (ownerName) { replaced = replaced.replace( new RegExp(escapeRegExp(ownerName), 'g'), FAKE_OWNER_USER.display_name, ); } return replaced; }; const transformRecordedData = (expectation, toSanitize) => { const requestBodySanitizer = httpRequest => { let body; if (httpRequest.body && httpRequest.body.type === 'JSON' && httpRequest.body.json) { const bodyObject = JSON.parse(httpRequest.body.json); if (bodyObject.encoding === 'base64') { // sanitize encoded data const decodedBody = Buffer.from(bodyObject.content, 'base64').toString('binary'); const sanitizedContent = sanitizeString(decodedBody, toSanitize); const sanitizedEncodedContent = Buffer.from(sanitizedContent, 'binary').toString('base64'); bodyObject.content = sanitizedEncodedContent; body = JSON.stringify(bodyObject); } else { body = httpRequest.body.json; } } else if (httpRequest.body && httpRequest.body.type === 'STRING' && httpRequest.body.string) { body = httpRequest.body.string; } else if ( httpRequest.body && httpRequest.body.type === 'BINARY' && httpRequest.body.base64Bytes ) { body = { encoding: 'base64', content: httpRequest.body.base64Bytes, contentType: httpRequest.body.contentType, }; } return body; }; const responseBodySanitizer = (httpRequest, httpResponse) => { let responseBody = null; if (httpResponse.body && httpResponse.body.string) { responseBody = httpResponse.body.string; } else if ( httpResponse.body && httpResponse.body.type === 'BINARY' && httpResponse.body.base64Bytes ) { responseBody = { encoding: 'base64', content: httpResponse.body.base64Bytes, }; } else if (httpResponse.body) { if (httpResponse.body && httpResponse.body.type === 'JSON' && httpResponse.body.json) { responseBody = JSON.stringify(httpResponse.body.json); } else { responseBody = httpResponse.body; } } // replace recorded user with fake one if ( responseBody && httpRequest.path === '/2.0/user' && httpRequest.headers.host.includes('api.bitbucket.org') ) { responseBody = JSON.stringify(FAKE_OWNER_USER); } return responseBody; }; return transformData(expectation, requestBodySanitizer, responseBodySanitizer); }; async function teardownBitBucketTest(taskData) { await resetRepositories(taskData); return null; } module.exports = { setupBitBucket, teardownBitBucket, setupBitBucketTest, teardownBitBucketTest, };