Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Mock } from 'vitest';
import { output } from '../../../../utils/output';
import { GithubRemoteReleaseClient } from './github';

vi.mock('axios', () => {
Expand All @@ -12,10 +13,16 @@ vi.mock('node:child_process', async () => ({
execSync: require('node:child_process').execSync,
}));

vi.mock('../../../../utils/prompt-helpers', () => ({
selectPrompt: vi.fn(),
}));

import { execFileSync } from 'node:child_process';
import { selectPrompt } from '../../../../utils/prompt-helpers';

const axiosGetMock = (await import('axios')).default.get as Mock;
const execFileSyncMock = execFileSync as Mock;
const selectPromptMock = selectPrompt as Mock;

describe('GithubRemoteReleaseClient', () => {
const client = new GithubRemoteReleaseClient(
Expand Down Expand Up @@ -168,4 +175,93 @@ describe('GithubRemoteReleaseClient', () => {
).resolves.toBeUndefined();
expect(authors.get('Test User')?.username).toBeUndefined();
});

describe('handleError', () => {
const repoData = {
hostname: 'github.com',
slug: 'nrwl/nx',
apiBaseUrl: 'https://api.github.com',
};

async function printedErrorBody(
client: GithubRemoteReleaseClient
): Promise<string> {
const errorSpy = vi.spyOn(output, 'error').mockImplementation(() => {});
selectPromptMock.mockResolvedValue('No');
const originalExitCode = process.exitCode;
try {
await (client as any).handleError(
{ response: { data: { message: 'Bad credentials' } } },
{ url: 'https://github.com/nrwl/nx/releases/new', requestData: {} }
);
} finally {
process.exitCode = originalExitCode;
}
expect(errorSpy).toHaveBeenCalledTimes(1);
const printed = errorSpy.mock.calls[0][0].bodyLines.join('\n');
errorSpy.mockRestore();
return printed;
}

it('should redact the token in the API error output', async () => {
const token = 'ghp_secret';
const clientWithToken = new GithubRemoteReleaseClient(repoData, false, {
token,
headerName: 'Authorization',
});

const printed = await printedErrorBody(clientWithToken);

expect(printed).not.toContain(token);
expect(printed).toContain(
'Token Header: Authorization: Bearer <redacted>'
);
});

it('should report when no token was configured', async () => {
const clientWithoutToken = new GithubRemoteReleaseClient(
repoData,
false,
null
);

const printed = await printedErrorBody(clientWithoutToken);

expect(printed).toContain('Token Header: none');
});

it('should redact the token in the unknown-error dump', async () => {
const token = 'ghp_secret';
const clientWithToken = new GithubRemoteReleaseClient(repoData, false, {
token,
headerName: 'Authorization',
});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
selectPromptMock.mockResolvedValue('No');
const originalExitCode = process.exitCode;

try {
await (clientWithToken as any).handleError(
{
message: 'Network Error',
config: { headers: { Authorization: `Bearer ${token}` } },
request: { _header: `Authorization: Bearer ${token}` },
},
{ url: 'https://github.com/nrwl/nx/releases/new', requestData: {} }
);
} finally {
process.exitCode = originalExitCode;
}

const logged = logSpy.mock.calls.map((args) => args.join(' ')).join('\n');
expect(logged).not.toContain(token);
expect(logged).toContain('<redacted>');
expect(logged).toContain('Network Error');
logSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,12 @@ export class GithubRemoteReleaseClient extends RemoteReleaseClient<GithubRemoteR
`---`,
`Request Data:`,
`Repo: ${this.getRemoteRepoData<GithubRepoData>()?.slug}`,
`Token Header Data: ${this.tokenHeader}`,
`Token Header: ${this.getRedactedTokenHeader()}`,
`Body: ${JSON.stringify(result.requestData)}`,
],
});
} else {
console.log(error);
console.log(this.inspectWithRedactedToken(error));
console.error(
`An unknown error occurred while trying to create a release on GitHub, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitHub token from the error message!)`
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { Mock } from 'vitest';
import { output } from '../../../../utils/output';
import { GitLabRemoteReleaseClient } from './gitlab';

vi.mock('../../../../utils/prompt-helpers', () => ({
selectPrompt: vi.fn(),
}));

import { selectPrompt } from '../../../../utils/prompt-helpers';

const selectPromptMock = selectPrompt as Mock;

describe('GitLabRemoteReleaseClient', () => {
afterEach(() => {
vi.resetAllMocks();
});

describe('handleError', () => {
const repoData = {
hostname: 'gitlab.com',
slug: 'nrwl/nx',
apiBaseUrl: 'https://gitlab.com/api/v4',
projectId: 'nrwl%2Fnx',
};

async function printedErrorBody(
client: GitLabRemoteReleaseClient
): Promise<string> {
const errorSpy = vi.spyOn(output, 'error').mockImplementation(() => {});
selectPromptMock.mockResolvedValue('No');
const originalExitCode = process.exitCode;
try {
await (client as any).handleError(
{ response: { data: { message: '401 Unauthorized' } } },
{ url: 'https://gitlab.com/nrwl/nx/-/releases/new', requestData: {} }
);
} finally {
process.exitCode = originalExitCode;
}
expect(errorSpy).toHaveBeenCalledTimes(1);
const printed = errorSpy.mock.calls[0][0].bodyLines.join('\n');
errorSpy.mockRestore();
return printed;
}

it('should redact the token in the API error output', async () => {
const token = 'glpat-secret';
const clientWithToken = new GitLabRemoteReleaseClient(repoData, false, {
token,
headerName: 'PRIVATE-TOKEN',
});

const printed = await printedErrorBody(clientWithToken);

expect(printed).not.toContain(token);
expect(printed).toContain('Token Header: PRIVATE-TOKEN: <redacted>');
});

it('should report when no token was configured', async () => {
const clientWithoutToken = new GitLabRemoteReleaseClient(
repoData,
false,
null
);

const printed = await printedErrorBody(clientWithoutToken);

expect(printed).toContain('Token Header: none');
});

it('should redact the token in the unknown-error dump', async () => {
const token = 'glpat-secret';
const clientWithToken = new GitLabRemoteReleaseClient(repoData, false, {
token,
headerName: 'PRIVATE-TOKEN',
});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
selectPromptMock.mockResolvedValue('No');
const originalExitCode = process.exitCode;

try {
await (clientWithToken as any).handleError(
{
message: 'Network Error',
config: { headers: { 'PRIVATE-TOKEN': token } },
request: { _header: `PRIVATE-TOKEN: ${token}` },
},
{ url: 'https://gitlab.com/nrwl/nx/-/releases/new', requestData: {} }
);
} finally {
process.exitCode = originalExitCode;
}

const logged = logSpy.mock.calls.map((args) => args.join(' ')).join('\n');
expect(logged).not.toContain(token);
expect(logged).toContain('<redacted>');
expect(logged).toContain('Network Error');
logSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,12 @@ export class GitLabRemoteReleaseClient extends RemoteReleaseClient<GitLabRelease
`---`,
`Request Data:`,
`Repo: ${this.getRemoteRepoData<GitLabRepoData>()?.slug}`,
`Token Header Data: ${this.tokenHeader}`,
`Token Header: ${this.getRedactedTokenHeader()}`,
`Body: ${JSON.stringify(result.requestData)}`,
],
});
} else {
console.log(error);
console.log(this.inspectWithRedactedToken(error));
console.error(
`An unknown error occurred while trying to create a release on GitLab, please report this on https://github.com/nrwl/nx (NOTE: make sure to redact your GitLab token from the error message!)`
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { inspect } from 'node:util';
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
import type { PostGitTask } from '../../changelog';
Expand Down Expand Up @@ -67,6 +68,23 @@ export abstract class RemoteReleaseClient<
return this.remoteRepoData as T | null;
}

protected inspectWithRedactedToken(error: unknown): string {
const inspected = inspect(error);
return this.tokenData
? inspected.split(this.tokenData.token).join('<redacted>')
: inspected;
}

protected getRedactedTokenHeader(): string {
if (!this.tokenData) {
return 'none';
}
const { headerName } = this.tokenData;
return headerName === 'Authorization'
? `${headerName}: Bearer <redacted>`
: `${headerName}: <redacted>`;
}

/**
* Create a post git task that will be executed by nx release changelog after performing any relevant
* git operations, if the user has opted into remote release creation.
Expand Down
Loading