This commit is contained in:
biss committed 2025-08-25 20:24:23 +08:00
1 parent 30106e0129
commit 0ae8d7a709
1044 files changed
+321581

No files matched your search

+363
View File
@@ -0,0 +1,363 @@
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 { retrieveRecordedExpectations, resetMockServerState } = require('../utils/mock-server');
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: {
Authorization: `Bearer ${token}`,
},
}).then(r => r.json());
}
function post(token, path, body) {
return fetch(`${API_URL}${path}`, {
method: 'POST',
...(body ? { body } : {}),
headers: {
...(body ? { 'Content-Type': 'application/json' } : {}),
Authorization: `Bearer ${token}`,
},
});
}
function del(token, path) {
return fetch(`${API_URL}${path}`, {
method: 'DELETE',
headers: {
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.log('Creating repository', testRepoName);
await post(token, `repositories/${owner}/${testRepoName}`, JSON.stringify({ scm: 'git' }));
const tempDir = path.join('.temp', testRepoName);
await fs.remove(tempDir);
let git = getGitClient();
const repoUrl = `git@bitbucket.org:${owner}/${repo}.git`;
console.log('Cloning repository', repoUrl);
await git.clone(repoUrl, tempDir);
git = getGitClient(tempDir);
console.log('Pushing to new repository', testRepoName);
await git.removeRemote('origin');
await git.addRemote(
'origin',
`https://x-token-auth:${token}@bitbucket.org/${owner}/${testRepoName}`,
);
await git.push(['-u', 'origin', 'master']);
if (lfs) {
console.log(`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', 'master');
}
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.log('Deleting repository', `${owner}/${repo}`);
await fs.remove(tempDir);
await del(token, `repositories/${owner}/${repo}`);
}
async function resetOriginRepo({ owner, repo, tempDir }) {
console.log('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.log('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 !== 'master').map(b => b.name);
console.log('Deleting branches', toDelete);
await Promise.all(
toDelete.map(branch => del(token, `repositories/${owner}/${repo}/refs/branches/${branch}`)),
);
console.log('Resetting master');
const git = getGitClient(tempDir);
await git.push(['--force', 'origin', 'master']);
console.log('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;
if (process.env.RECORD_FIXTURES) {
console.log('Running tests in "record" mode - live data with be used!');
const [user, repoData] = await Promise.all([getUser(), prepareTestBitBucketRepo({ lfs })]);
await updateConfig(config => {
merge(config, rest, {
backend: {
repo: `${repoData.owner}/${repoData.repo}`,
},
});
});
return { ...repoData, user, mockResponses: false };
} else {
console.log('Running tests in "playback" mode - local data with be used');
await updateConfig(config => {
merge(config, rest, {
backend: {
repo: `${BITBUCKET_REPO_OWNER_SANITIZED_VALUE}/${BITBUCKET_REPO_NAME_SANITIZED_VALUE}`,
},
});
});
return {
owner: BITBUCKET_REPO_OWNER_SANITIZED_VALUE,
repo: BITBUCKET_REPO_NAME_SANITIZED_VALUE,
user: {
...FAKE_OWNER_USER,
token: BITBUCKET_REPO_TOKEN_SANITIZED_VALUE,
backendName: 'bitbucket',
},
mockResponses: true,
};
}
}
async function teardownBitBucket(taskData) {
if (process.env.RECORD_FIXTURES) {
await deleteRepositories(taskData);
}
return null;
}
async function setupBitBucketTest(taskData) {
if (process.env.RECORD_FIXTURES) {
await resetRepositories(taskData);
await resetMockServerState();
}
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) {
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;
};
const cypressRouteOptions = transformData(
expectation,
requestBodySanitizer,
responseBodySanitizer,
);
return cypressRouteOptions;
};
async function teardownBitBucketTest(taskData) {
if (process.env.RECORD_FIXTURES) {
await resetRepositories(taskData);
try {
const filename = getExpectationsFilename(taskData);
console.log('Persisting recorded data for test:', path.basename(filename));
const { owner, token } = await getEnvs();
const expectations = await retrieveRecordedExpectations();
const toSanitize = {
owner,
repo: taskData.repo,
token,
ownerName: taskData.user.name,
};
// transform the mock proxy recorded requests into Cypress route format
const toPersist = expectations.map(expectation =>
transformRecordedData(expectation, toSanitize),
);
const toPersistString = sanitizeString(JSON.stringify(toPersist, null, 2), toSanitize);
await fs.writeFile(filename, toPersistString);
} catch (e) {
console.log(e);
}
await resetMockServerState();
}
return null;
}
module.exports = {
setupBitBucket,
teardownBitBucket,
setupBitBucketTest,
teardownBitBucketTest,
};
+78
View File
@@ -0,0 +1,78 @@
const path = require('path');
const { default: simpleGit } = require('simple-git');
const GIT_SSH_COMMAND = 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no';
const GIT_SSL_NO_VERIFY = true;
const getExpectationsFilename = taskData => {
const { spec, testName } = taskData;
const basename = `${spec}__${testName}`;
const fixtures = path.join(__dirname, '..', 'fixtures');
const filename = path.join(fixtures, `${basename}.json`);
return filename;
};
const HEADERS_TO_IGNORE = [
'Date',
'X-RateLimit-Remaining',
'X-RateLimit-Reset',
'ETag',
'Last-Modified',
'X-GitHub-Request-Id',
'X-NF-Request-ID',
'X-Request-Id',
'X-Runtime',
'RateLimit-Limit',
'RateLimit-Observed',
'RateLimit-Remaining',
'RateLimit-Reset',
'RateLimit-ResetTime',
'GitLab-LB',
].map(h => h.toLowerCase());
const transformRecordedData = (expectation, requestBodySanitizer, responseBodySanitizer) => {
const { httpRequest, httpResponse } = expectation;
const responseHeaders = {};
Object.keys(httpResponse.headers)
.filter(key => !HEADERS_TO_IGNORE.includes(key.toLowerCase()))
.forEach(key => {
responseHeaders[key] = httpResponse.headers[key][0];
});
let queryString;
if (httpRequest.queryStringParameters) {
const { queryStringParameters } = httpRequest;
queryString = Object.keys(queryStringParameters)
.map(key => `${key}=${queryStringParameters[key]}`)
.join('&');
}
const body = requestBodySanitizer(httpRequest);
const responseBody = responseBodySanitizer(httpRequest, httpResponse);
const cypressRouteOptions = {
body,
method: httpRequest.method,
url: queryString ? `${httpRequest.path}?${queryString}` : httpRequest.path,
headers: responseHeaders,
response: responseBody,
status: httpResponse.statusCode,
};
return cypressRouteOptions;
};
function getGitClient(repoDir) {
const git = simpleGit(repoDir).env({ ...process.env, GIT_SSH_COMMAND, GIT_SSL_NO_VERIFY });
return git;
}
module.exports = {
getExpectationsFilename,
transformRecordedData,
getGitClient,
};
+327
View File
@@ -0,0 +1,327 @@
const fetch = require('node-fetch');
const {
transformRecordedData: transformGitHub,
setupGitHub,
teardownGitHub,
setupGitHubTest,
teardownGitHubTest,
} = require('./github');
const {
transformRecordedData: transformGitLab,
setupGitLab,
teardownGitLab,
setupGitLabTest,
teardownGitLabTest,
} = require('./gitlab');
const { getGitClient } = require('./common');
function getEnvs() {
const {
NETLIFY_API_TOKEN: netlifyApiToken,
GITHUB_REPO_TOKEN: githubToken,
GITLAB_REPO_TOKEN: gitlabToken,
NETLIFY_INSTALLATION_ID: installationId,
} = process.env;
if (!netlifyApiToken) {
throw new Error(
'Please set NETLIFY_API_TOKEN, GITHUB_REPO_TOKEN, GITLAB_REPO_TOKEN, NETLIFY_INSTALLATION_ID environment variables',
);
}
return { netlifyApiToken, githubToken, gitlabToken, installationId };
}
const apiRoot = 'https://api.netlify.com/api/v1/';
async function get(netlifyApiToken, path) {
const response = await fetch(`${apiRoot}${path}`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${netlifyApiToken}`,
},
}).then(res => res.json());
return response;
}
async function post(netlifyApiToken, path, payload) {
const response = await fetch(`${apiRoot}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${netlifyApiToken}`,
},
...(payload ? { body: JSON.stringify(payload) } : {}),
}).then(res => res.json());
return response;
}
async function del(netlifyApiToken, path) {
const response = await fetch(`${apiRoot}${path}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${netlifyApiToken}`,
},
}).then(res => res.text());
return response;
}
async function createSite(netlifyApiToken, payload) {
return post(netlifyApiToken, 'sites', payload);
}
async function enableIdentity(netlifyApiToken, siteId) {
return post(netlifyApiToken, `sites/${siteId}/identity`, {});
}
async function enableGitGateway(netlifyApiToken, siteId, provider, token, repo) {
return post(netlifyApiToken, `sites/${siteId}/services/git/instances`, {
[provider]: {
repo,
access_token: token,
},
});
}
async function enableLargeMedia(netlifyApiToken, siteId) {
return post(netlifyApiToken, `sites/${siteId}/services/large-media/instances`, {});
}
async function waitForDeploys(netlifyApiToken, siteId) {
for (let i = 0; i < 10; i++) {
const deploys = await get(netlifyApiToken, `sites/${siteId}/deploys`);
if (deploys.some(deploy => deploy.state === 'ready')) {
console.log('Deploy finished for site:', siteId);
return;
}
console.log('Waiting on deploy of site:', siteId);
await new Promise(resolve => setTimeout(resolve, 30 * 1000));
}
console.log('Timed out waiting on deploy of site:', siteId);
}
async function createUser(netlifyApiToken, siteUrl, email, password) {
const response = await fetch(`${siteUrl}/.netlify/functions/create-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${netlifyApiToken}`,
},
body: JSON.stringify({ email, password }),
});
if (response.ok) {
console.log('User created successfully');
} else {
throw new Error('Failed to create user');
}
}
const netlifySiteURL = 'https://fake-site-url.netlify.com/';
const email = 'decap@p-m.si';
const password = '12345678';
const backendName = 'git-gateway';
const methods = {
github: {
setup: setupGitHub,
teardown: teardownGitHub,
setupTest: setupGitHubTest,
teardownTest: teardownGitHubTest,
transformData: transformGitHub,
createSite: (netlifyApiToken, result) => {
const { installationId } = getEnvs();
return createSite(netlifyApiToken, {
repo: {
provider: 'github',
installation_id: installationId,
repo: `${result.owner}/${result.repo}`,
},
});
},
token: () => getEnvs().githubToken,
},
gitlab: {
setup: setupGitLab,
teardown: teardownGitLab,
setupTest: setupGitLabTest,
teardownTest: teardownGitLabTest,
transformData: transformGitLab,
createSite: async (netlifyApiToken, result) => {
const { id, public_key } = await post(netlifyApiToken, 'deploy_keys');
const { gitlabToken } = getEnvs();
const project = `${result.owner}/${result.repo}`;
await fetch(`https://gitlab.com/api/v4/projects/${encodeURIComponent(project)}/deploy_keys`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${gitlabToken}`,
},
body: JSON.stringify({ title: 'Netlify Deploy Key', key: public_key, can_push: false }),
}).then(res => res.json());
const site = await createSite(netlifyApiToken, {
account_slug: result.owner,
repo: {
provider: 'gitlab',
repo: `${result.owner}/${result.repo}`,
deploy_key_id: id,
},
});
await fetch(`https://gitlab.com/api/v4/projects/${encodeURIComponent(project)}/hooks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${gitlabToken}`,
},
body: JSON.stringify({
url: 'https://api.netlify.com/hooks/gitlab',
push_events: true,
merge_requests_events: true,
enable_ssl_verification: true,
}),
}).then(res => res.json());
return site;
},
token: () => getEnvs().gitlabToken,
},
};
async function setupGitGateway(options) {
const { provider, ...rest } = options;
const result = await methods[provider].setup(rest);
if (process.env.RECORD_FIXTURES) {
const { netlifyApiToken } = getEnvs();
console.log(`Creating Netlify Site for provider: ${provider}`);
let site_id, ssl_url;
try {
({ site_id, ssl_url } = await methods[provider].createSite(netlifyApiToken, result));
} catch (e) {
console.log(e);
throw e;
}
console.log('Enabling identity for site:', site_id);
await enableIdentity(netlifyApiToken, site_id);
console.log('Enabling git gateway for site:', site_id);
const token = methods[provider].token();
await enableGitGateway(
netlifyApiToken,
site_id,
provider,
token,
`${result.owner}/${result.repo}`,
);
console.log('Enabling large media for site:', site_id);
await enableLargeMedia(netlifyApiToken, site_id);
const git = getGitClient(result.tempDir);
await git.raw([
'config',
'-f',
'.lfsconfig',
'lfs.url',
`https://${site_id}.netlify.com/.netlify/large-media`,
]);
await git.addConfig('commit.gpgsign', 'false');
await git.add('.lfsconfig');
await git.commit('add .lfsconfig');
await git.push('origin', 'master');
await waitForDeploys(netlifyApiToken, site_id);
console.log('Creating user for site:', site_id, 'with email:', email);
try {
await createUser(netlifyApiToken, ssl_url, email, password);
} catch (e) {
console.log(e);
}
return {
...result,
user: {
...result.user,
backendName,
netlifySiteURL: ssl_url,
email,
password,
},
site_id,
ssl_url,
provider,
};
} else {
return {
...result,
user: {
...result.user,
backendName,
netlifySiteURL,
email,
password,
},
provider,
};
}
}
async function teardownGitGateway(taskData) {
if (process.env.RECORD_FIXTURES) {
const { netlifyApiToken } = getEnvs();
const { site_id } = taskData;
console.log('Deleting Netlify site:', site_id);
await del(netlifyApiToken, `sites/${site_id}`);
const result = await methods[taskData.provider].teardown(taskData);
return result;
}
return null;
}
async function setupGitGatewayTest(taskData) {
if (process.env.RECORD_FIXTURES) {
const result = await methods[taskData.provider].setupTest(taskData);
return result;
}
return null;
}
async function teardownGitGatewayTest(taskData) {
if (process.env.RECORD_FIXTURES) {
const options = {
transformRecordedData: (expectation, toSanitize) => {
const result = methods[taskData.provider].transformData(expectation, toSanitize);
if (result.response && result.url === '/.netlify/identity/token') {
const parsed = JSON.parse(result.response);
parsed.access_token = 'access_token';
parsed.refresh_token = 'refresh_token';
return { ...result, response: JSON.stringify(parsed) };
} else {
return result;
}
},
};
const result = await methods[taskData.provider].teardownTest(taskData, options);
return result;
}
return null;
}
module.exports = {
setupGitGateway,
teardownGitGateway,
setupGitGatewayTest,
teardownGitGatewayTest,
};
Loaded 3 of 1044 files, more files were not shown because too many files have changed in this diff. Show more