blob: cb4a43db934ef9eb397304cd16378e0d9e76bbe4 [file] [log] [blame]
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../test/common-test-setup';
import {
addListenerForTest,
assertFails,
makePrefixedJSON,
MockPromise,
mockPromise,
waitEventLoop,
} from '../../test/test-utils';
import {GrReviewerUpdatesParser} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
import {listChangesOptionsToHex} from '../../utils/change-util';
import {
createAccountDetailWithId,
createAccountWithId,
createChange,
createComment,
createEditInfo,
createFixReplacementInfo,
createParsedChange,
createServerInfo,
TEST_PROJECT_NAME,
} from '../../test/test-data-generators';
import {CURRENT} from '../../utils/patch-set-util';
import {GrRestApiServiceImpl} from './gr-rest-api-impl';
import {
CommentSide,
createDefaultEditPrefs,
HttpMethod,
} from '../../constants/constants';
import {
AccountDetailInfo,
BasePatchSetNum,
ChangeInfo,
ChangeMessageId,
CommentInfo,
CommentInput,
DashboardId,
EDIT,
Hashtag,
ListChangesOption,
NumericChangeId,
PARENT,
ParsedJSON,
PatchSetNum,
RepoName,
RevisionPatchSetNum,
RobotCommentInfo,
Timestamp,
UrlEncodedCommentId,
} from '../../types/common';
import {assert} from '@open-wc/testing';
import {AuthService} from '../gr-auth/gr-auth';
import {GrAuthMock} from '../gr-auth/gr-auth_mock';
import {FlagsServiceImplementation} from '../flags/flags_impl';
const EXPECTED_QUERY_OPTIONS = listChangesOptionsToHex(
ListChangesOption.CHANGE_ACTIONS,
// Current actions can be costly to calculate (e.g submit action)
// They are not used in bulk actions.
// ListChangesOption.CURRENT_ACTIONS,
ListChangesOption.CURRENT_REVISION,
ListChangesOption.DETAILED_LABELS,
ListChangesOption.SUBMIT_REQUIREMENTS
);
suite('gr-rest-api-service-impl tests', () => {
let element: GrRestApiServiceImpl;
let authService: AuthService;
let ctr = 0;
let originalCanonicalPath: string | undefined;
setup(() => {
// Modify CANONICAL_PATH to effectively reset cache.
ctr += 1;
originalCanonicalPath = window.CANONICAL_PATH;
window.CANONICAL_PATH = `test${ctr}`;
const testJSON = ')]}\'\n{"hello": "bonjour"}';
sinon.stub(window, 'fetch').resolves(new Response(testJSON));
// fake auth
authService = new GrAuthMock();
sinon.stub(authService, 'authCheck').resolves(true);
element = new GrRestApiServiceImpl(
authService,
new FlagsServiceImplementation()
);
element._projectLookup = {};
});
teardown(() => {
window.CANONICAL_PATH = originalCanonicalPath;
});
test('parent diff comments are properly grouped', async () => {
element.addRepoNameToCache(42 as NumericChangeId, TEST_PROJECT_NAME);
sinon.stub(element._restApiHelper, 'fetchJSON').resolves({
'/COMMIT_MSG': [],
'sieve.go': [
{
updated: '2017-02-03 22:32:28.000000000',
message: 'this isn’t quite right',
},
{
side: CommentSide.PARENT,
message: 'how did this work in the first place?',
updated: '2017-02-03 22:33:28.000000000',
},
],
} as unknown as ParsedJSON);
const obj = await element._getDiffComments(
42 as NumericChangeId,
'/comments',
undefined,
PARENT,
1 as PatchSetNum,
'sieve.go'
);
assert.equal(obj.baseComments.length, 1);
assert.deepEqual(obj.baseComments[0], {
side: CommentSide.PARENT,
message: 'how did this work in the first place?',
path: 'sieve.go',
updated: '2017-02-03 22:33:28.000000000' as Timestamp,
} as RobotCommentInfo);
assert.equal(obj.comments.length, 1);
assert.deepEqual(obj.comments[0], {
message: 'this isn’t quite right',
path: 'sieve.go',
updated: '2017-02-03 22:32:28.000000000' as Timestamp,
} as RobotCommentInfo);
});
test('_setRange', () => {
const comments: CommentInfo[] = [
{
id: '1' as UrlEncodedCommentId,
side: CommentSide.PARENT,
message: 'how did this work in the first place?',
updated: '2017-02-03 22:32:28.000000000' as Timestamp,
range: {
start_line: 1,
start_character: 1,
end_line: 2,
end_character: 1,
},
},
{
id: '2' as UrlEncodedCommentId,
in_reply_to: '1' as UrlEncodedCommentId,
message: 'this isn’t quite right',
updated: '2017-02-03 22:33:28.000000000' as Timestamp,
},
];
const expectedResult: CommentInfo = {
id: '2' as UrlEncodedCommentId,
in_reply_to: '1' as UrlEncodedCommentId,
message: 'this isn’t quite right',
updated: '2017-02-03 22:33:28.000000000' as Timestamp,
range: {
start_line: 1,
start_character: 1,
end_line: 2,
end_character: 1,
},
};
const comment = comments[1];
assert.deepEqual(element._setRange(comments, comment), expectedResult);
});
test('_setRanges', () => {
const comments: CommentInfo[] = [
{
id: '3' as UrlEncodedCommentId,
in_reply_to: '2' as UrlEncodedCommentId,
message: 'this isn’t quite right either',
updated: '2017-02-03 22:34:28.000000000' as Timestamp,
},
{
id: '2' as UrlEncodedCommentId,
in_reply_to: '1' as UrlEncodedCommentId,
message: 'this isn’t quite right',
updated: '2017-02-03 22:33:28.000000000' as Timestamp,
},
{
id: '1' as UrlEncodedCommentId,
side: CommentSide.PARENT,
message: 'how did this work in the first place?',
updated: '2017-02-03 22:32:28.000000000' as Timestamp,
range: {
start_line: 1,
start_character: 1,
end_line: 2,
end_character: 1,
},
},
];
const expectedResult: CommentInfo[] = [
{
id: '1' as UrlEncodedCommentId,
side: CommentSide.PARENT,
message: 'how did this work in the first place?',
updated: '2017-02-03 22:32:28.000000000' as Timestamp,
range: {
start_line: 1,
start_character: 1,
end_line: 2,
end_character: 1,
},
},
{
id: '2' as UrlEncodedCommentId,
in_reply_to: '1' as UrlEncodedCommentId,
message: 'this isn’t quite right',
updated: '2017-02-03 22:33:28.000000000' as Timestamp,
range: {
start_line: 1,
start_character: 1,
end_line: 2,
end_character: 1,
},
},
{
id: '3' as UrlEncodedCommentId,
in_reply_to: '2' as UrlEncodedCommentId,
message: 'this isn’t quite right either',
updated: '2017-02-03 22:34:28.000000000' as Timestamp,
range: {
start_line: 1,
start_character: 1,
end_line: 2,
end_character: 1,
},
},
];
assert.deepEqual(element._setRanges(comments), expectedResult);
});
test('differing patch diff comments are properly grouped', async () => {
sinon.stub(element, 'getRepoName').resolves('test' as RepoName);
sinon.stub(element._restApiHelper, 'fetchJSON').callsFake(async request => {
const url = request.url;
if (url === '/changes/test~42/revisions/1/comments') {
return {
'/COMMIT_MSG': [],
'sieve.go': [
{
message: 'this isn’t quite right',
updated: '2017-02-03 22:32:28.000000000',
},
{
side: CommentSide.PARENT,
message: 'how did this work in the first place?',
updated: '2017-02-03 22:33:28.000000000',
},
],
} as unknown as ParsedJSON;
} else if (url === '/changes/test~42/revisions/2/comments') {
return {
'/COMMIT_MSG': [],
'sieve.go': [
{
message: 'What on earth are you thinking, here?',
updated: '2017-02-03 22:32:28.000000000',
},
{
side: CommentSide.PARENT,
message: 'Yeah not sure how this worked either?',
updated: '2017-02-03 22:33:28.000000000',
},
{
message: '¯\\_(ツ)_/¯',
updated: '2017-02-04 22:33:28.000000000',
},
],
} as unknown as ParsedJSON;
}
return undefined;
});
const obj = await element._getDiffComments(
42 as NumericChangeId,
'/comments',
undefined,
1 as BasePatchSetNum,
2 as PatchSetNum,
'sieve.go'
);
assert.equal(obj.baseComments.length, 1);
assert.deepEqual(obj.baseComments[0], {
message: 'this isn’t quite right',
path: 'sieve.go',
updated: '2017-02-03 22:32:28.000000000' as Timestamp,
} as RobotCommentInfo);
assert.equal(obj.comments.length, 2);
assert.deepEqual(obj.comments[0], {
message: 'What on earth are you thinking, here?',
path: 'sieve.go',
updated: '2017-02-03 22:32:28.000000000' as Timestamp,
} as RobotCommentInfo);
assert.deepEqual(obj.comments[1], {
message: '¯\\_(ツ)_/¯',
path: 'sieve.go',
updated: '2017-02-04 22:33:28.000000000' as Timestamp,
} as RobotCommentInfo);
});
test('legacy n,z key in change url is replaced', async () => {
const stub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves([] as unknown as ParsedJSON);
await element.getChanges(1, undefined, 'n,z');
assert.equal(stub.lastCall.args[0].params!.S, 0);
});
test('saveDiffPreferences invalidates cache line', () => {
const cacheKey = '/accounts/self/preferences.diff';
const fetchStub = sinon.stub(element._restApiHelper, 'fetch');
element._cache.set(cacheKey, {tab_size: 4} as unknown as ParsedJSON);
element.saveDiffPreferences({
tab_size: 8,
ignore_whitespace: 'IGNORE_NONE',
});
assert.isTrue(fetchStub.called);
assert.isFalse(element._cache.has(cacheKey));
});
suite('queryAccounts', () => {
let fetchStub: sinon.SinonStub;
const testProject = 'testproject';
const testChangeNumber = 341682;
setup(() => {
fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(makePrefixedJSON(createAccountWithId())));
element.addRepoNameToCache(
testChangeNumber as NumericChangeId,
testProject as RepoName
);
});
test('url with just email', async () => {
await element.queryAccounts('bro');
assert.isTrue(fetchStub.calledOnce);
assert.deepEqual(fetchStub.firstCall.args[0].params, {
o: 'DETAILS',
q: '"bro"',
});
});
test('url with email and canSee changeId', async () => {
await element.queryAccounts(
'bro',
undefined,
testChangeNumber as NumericChangeId
);
assert.isTrue(fetchStub.calledOnce);
assert.deepEqual(fetchStub.firstCall.args[0].params, {
o: 'DETAILS',
q: `"bro" and cansee:${testProject}~${testChangeNumber}`,
});
});
test('url with email and canSee changeId and isActive', async () => {
await element.queryAccounts(
'bro',
undefined,
testChangeNumber as NumericChangeId,
true
);
assert.isTrue(fetchStub.calledOnce);
assert.deepEqual(fetchStub.firstCall.args[0].params, {
o: 'DETAILS',
q: `"bro" and cansee:${testProject}~${testChangeNumber} and is:active`,
});
});
});
test('getAccountSuggestions using suggest query param', () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response());
element.getAccountSuggestions('user');
assert.isTrue(fetchStub.calledOnce);
assert.deepEqual(fetchStub.firstCall.args[0].params, {
suggest: undefined,
q: 'user',
});
});
test('getAccount when resp is undefined clears cache', async () => {
const cacheKey = '/accounts/self/detail';
const account = createAccountDetailWithId();
element._cache.set(cacheKey, account as unknown as ParsedJSON);
const stub = sinon
.stub(element._restApiHelper, 'fetchCacheJSON')
.callsFake(async req => {
req.errFn!(undefined);
return undefined;
});
assert.isTrue(element._cache.has(cacheKey));
await element.getAccount();
assert.isTrue(stub.called);
assert.isFalse(element._cache.has(cacheKey));
});
test('getAccount when status is 403 clears cache', async () => {
const cacheKey = '/accounts/self/detail';
const account = createAccountDetailWithId();
element._cache.set(cacheKey, account as unknown as ParsedJSON);
const stub = sinon
.stub(element._restApiHelper, 'fetchCacheJSON')
.callsFake(async req => {
req.errFn!(new Response(undefined, {status: 403}));
return undefined;
});
assert.isTrue(element._cache.has(cacheKey));
await element.getAccount();
assert.isTrue(stub.called);
assert.isFalse(element._cache.has(cacheKey));
});
test('getAccount when resp is successful updates cache', async () => {
const cacheKey = '/accounts/self/detail';
const account = createAccountDetailWithId();
const stub = sinon
.stub(element._restApiHelper, 'fetchCacheJSON')
.callsFake(async () => {
element._cache.set(cacheKey, account as unknown as ParsedJSON);
return undefined;
});
assert.isFalse(element._cache.has(cacheKey));
await element.getAccount();
assert.isTrue(stub.called);
assert.equal(
element._cache.get(cacheKey),
account as unknown as ParsedJSON
);
});
const preferenceSetup = function (testJSON: unknown, loggedIn: boolean) {
sinon
.stub(element, 'getLoggedIn')
.callsFake(() => Promise.resolve(loggedIn));
sinon
.stub(element._restApiHelper, 'fetchCacheJSON')
.callsFake(() => Promise.resolve(testJSON as ParsedJSON));
};
test('getPreferences returns correctly logged in', async () => {
const testJSON = {diff_view: 'SIDE_BY_SIDE'};
const loggedIn = true;
preferenceSetup(testJSON, loggedIn);
const obj = await element.getPreferences();
assert.equal(obj!.diff_view, 'SIDE_BY_SIDE');
});
test('getPreferences returns correctly on larger screens logged in', async () => {
const testJSON = {diff_view: 'UNIFIED_DIFF'};
const loggedIn = true;
preferenceSetup(testJSON, loggedIn);
const obj = await element.getPreferences();
assert.equal(obj!.diff_view, 'UNIFIED_DIFF');
});
test('getPreferences returns correctly on larger screens no login', async () => {
const testJSON = {diff_view: 'UNIFIED_DIFF'};
const loggedIn = false;
preferenceSetup(testJSON, loggedIn);
const obj = await element.getPreferences();
assert.equal(obj!.diff_view, 'SIDE_BY_SIDE');
});
test('savePreferences normalizes download scheme', () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response());
element.savePreferences({download_scheme: 'HTTP'});
assert.isTrue(fetchStub.called);
assert.equal(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string)
.download_scheme,
'http'
);
});
test('getDiffPreferences returns correct defaults', async () => {
sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
const obj = (await element.getDiffPreferences())!;
assert.equal(obj.context, 10);
assert.equal(obj.cursor_blink_rate, 0);
assert.equal(obj.font_size, 12);
assert.equal(obj.ignore_whitespace, 'IGNORE_NONE');
assert.equal(obj.line_length, 100);
assert.equal(obj.line_wrapping, false);
assert.equal(obj.show_line_endings, true);
assert.equal(obj.show_tabs, true);
assert.equal(obj.show_whitespace_errors, true);
assert.equal(obj.syntax_highlighting, true);
assert.equal(obj.tab_size, 8);
});
test('saveDiffPreferences set show_tabs to false', () => {
const fetchStub = sinon.stub(element._restApiHelper, 'fetch');
element.saveDiffPreferences({
show_tabs: false,
ignore_whitespace: 'IGNORE_NONE',
});
assert.isTrue(fetchStub.called);
assert.equal(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string)
.show_tabs,
false
);
});
test('getEditPreferences returns correct defaults', async () => {
sinon.stub(element, 'getLoggedIn').callsFake(() => Promise.resolve(false));
const obj = (await element.getEditPreferences())!;
assert.equal(obj.auto_close_brackets, false);
assert.equal(obj.cursor_blink_rate, 0);
assert.equal(obj.hide_line_numbers, false);
assert.equal(obj.hide_top_menu, false);
assert.equal(obj.indent_unit, 2);
assert.equal(obj.indent_with_tabs, false);
assert.equal(obj.key_map_type, 'DEFAULT');
assert.equal(obj.line_length, 100);
assert.equal(obj.line_wrapping, false);
assert.equal(obj.match_brackets, true);
assert.equal(obj.show_base, false);
assert.equal(obj.show_tabs, true);
assert.equal(obj.show_whitespace_errors, true);
assert.equal(obj.syntax_highlighting, true);
assert.equal(obj.tab_size, 8);
assert.equal(obj.theme, 'DEFAULT');
});
test('saveEditPreferences set show_tabs to false', () => {
const fetchStub = sinon.stub(element._restApiHelper, 'fetch');
element.saveEditPreferences({
...createDefaultEditPrefs(),
show_tabs: false,
});
assert.isTrue(fetchStub.called);
assert.equal(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string)
.show_tabs,
false
);
});
test('confirmEmail', () => {
const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
element.confirmEmail('foo');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(
fetchStub.lastCall.args[0].url,
'/config/server/email.confirm'
);
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{token: 'foo'}
);
});
test('setPreferredAccountEmail', async () => {
const email1 = 'email1@example.com';
const email2 = 'email2@example.com';
const encodedEmail = encodeURIComponent(email2);
const sendStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
element._cache.set('/accounts/self/emails', [
{email: email1, preferred: true},
{email: email2, preferred: false},
] as unknown as ParsedJSON);
await element.setPreferredAccountEmail(email2);
assert.isTrue(sendStub.calledOnce);
assert.equal(
sendStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(
sendStub.lastCall.args[0].url,
`/accounts/self/emails/${encodedEmail}/preferred`
);
assert.deepEqual(element._cache.get('/accounts/self/emails'), [
{email: email1, preferred: false},
{email: email2, preferred: true},
] as unknown as ParsedJSON);
});
test('setAccountUsername', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(makePrefixedJSON('john')));
element._cache.set(
'/accounts/self/detail',
createAccountDetailWithId() as unknown as ParsedJSON
);
await element.setAccountUsername('john');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(fetchStub.lastCall.args[0].url, '/accounts/self/username');
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{username: 'john'}
);
assert.deepEqual(
(element._cache.get(
'/accounts/self/detail'
) as unknown as AccountDetailInfo)!.username,
'john'
);
});
test('setAccountUsername empty unsets field', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(undefined, {status: 204}));
element._cache.set('/accounts/self/detail', {
...createAccountDetailWithId(),
username: 'john',
} as unknown as ParsedJSON);
await element.setAccountUsername('');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.isUndefined(
(element._cache.get(
'/accounts/self/detail'
) as unknown as AccountDetailInfo)!.username
);
});
test('setAccountDisplayName', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(makePrefixedJSON('john')));
element._cache.set(
'/accounts/self/detail',
createAccountDetailWithId() as unknown as ParsedJSON
);
await element.setAccountDisplayName('john');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(fetchStub.lastCall.args[0].url, '/accounts/self/displayname');
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{display_name: 'john'}
);
assert.deepEqual(
(element._cache.get(
'/accounts/self/detail'
) as unknown as AccountDetailInfo)!.display_name,
'john'
);
});
test('setAccountDisplayName empty unsets field', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(undefined, {status: 204}));
element._cache.set('/accounts/self/detail', {
...createAccountDetailWithId(),
display_name: 'john',
} as unknown as ParsedJSON);
await element.setAccountDisplayName('');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.isUndefined(
(element._cache.get(
'/accounts/self/detail'
) as unknown as AccountDetailInfo)!.display_name
);
});
test('setAccountName', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(makePrefixedJSON('john')));
element._cache.set(
'/accounts/self/detail',
createAccountDetailWithId() as unknown as ParsedJSON
);
await element.setAccountName('john');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(fetchStub.lastCall.args[0].url, '/accounts/self/name');
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{name: 'john'}
);
assert.deepEqual(
(element._cache.get(
'/accounts/self/detail'
) as unknown as AccountDetailInfo)!.name,
'john'
);
});
test('setAccountName empty unsets field', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(undefined, {status: 204}));
element._cache.set('/accounts/self/detail', {
...createAccountDetailWithId(),
name: 'john',
} as unknown as ParsedJSON);
await element.setAccountName('');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.isUndefined(
(element._cache.get(
'/accounts/self/detail'
) as unknown as AccountDetailInfo)!.name
);
});
test('setAccountStatus', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(makePrefixedJSON('OOO')));
element._cache.set(
'/accounts/self/detail',
createAccountDetailWithId() as unknown as ParsedJSON
);
await element.setAccountStatus('OOO');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(fetchStub.lastCall.args[0].url, '/accounts/self/status');
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{status: 'OOO'}
);
assert.deepEqual(
(element._cache.get(
'/accounts/self/detail'
) as unknown as AccountDetailInfo)!.status,
'OOO'
);
});
test('setAccountStatus empty unsets field', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(undefined, {status: 204}));
element._cache.set('/accounts/self/detail', {
...createAccountDetailWithId(),
status: 'OOO',
} as unknown as ParsedJSON);
await element.setAccountStatus('');
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.isUndefined(
(element._cache.get(
'/accounts/self/detail'
) as unknown as AccountDetailInfo)!.status
);
});
suite('draft comments', () => {
test('_sendDiffDraftRequest pending requests tracked', async () => {
const obj = element._pendingRequests;
const promises: MockPromise<string>[] = [];
sinon.stub(element, '_changeBaseURL').callsFake(() => {
promises.push(mockPromise<string>());
return promises[promises.length - 1];
});
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(undefined, {status: 201}));
const draft: CommentInput = {
id: 'draft-id' as UrlEncodedCommentId,
message: 'draft message',
};
assert.isFalse(!!element.hasPendingDiffDrafts());
element._sendDiffDraftRequest(
HttpMethod.PUT,
123 as NumericChangeId,
1 as PatchSetNum,
draft
);
assert.equal(obj.sendDiffDraft.length, 1);
assert.isTrue(!!element.hasPendingDiffDrafts());
element._sendDiffDraftRequest(
HttpMethod.PUT,
123 as NumericChangeId,
1 as PatchSetNum,
draft
);
assert.equal(obj.sendDiffDraft.length, 2);
assert.isTrue(!!element.hasPendingDiffDrafts());
for (const promise of promises) {
promise.resolve('');
}
await element.awaitPendingDiffDrafts();
assert.equal(obj.sendDiffDraft.length, 0);
assert.isFalse(!!element.hasPendingDiffDrafts());
assert.isTrue(fetchStub.called);
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
draft
);
});
suite('_failForCreate200', () => {
test('_sendDiffDraftRequest checks for 200 on create', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
sinon.stub(element._restApiHelper, 'fetch').resolves(new Response());
const failStub = sinon.stub(element, '_failForCreate200').resolves();
await element._sendDiffDraftRequest(
HttpMethod.PUT,
123 as NumericChangeId,
4 as PatchSetNum,
{}
);
assert.isTrue(failStub.calledOnce);
});
test('_sendDiffDraftRequest no checks for 200 on non create', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
sinon.stub(element._restApiHelper, 'fetch').resolves(new Response());
const failStub = sinon.stub(element, '_failForCreate200').resolves();
await element._sendDiffDraftRequest(
HttpMethod.PUT,
123 as NumericChangeId,
4 as PatchSetNum,
{
id: '123' as UrlEncodedCommentId,
}
);
assert.isFalse(failStub.called);
});
test('_failForCreate200 fails on 200', async () => {
const result = new Response(undefined, {
status: 200,
headers: {
'Set-CoOkiE': 'secret',
Innocuous: 'hello',
},
});
const error = await assertFails<Error>(
element._failForCreate200(Promise.resolve(result))
);
assert.isOk(error);
assert.include(error.message, 'Saving draft resulted in HTTP 200');
assert.include(error.message, 'hello');
assert.notInclude(error.message, 'secret');
});
test('_failForCreate200 does not fail on 201', () => {
const result = new Response(undefined, {status: 201});
return element._failForCreate200(Promise.resolve(result));
});
});
});
test('saveChangeEdit', async () => {
element._projectLookup = {1: Promise.resolve('test' as RepoName)};
const change_num = 1 as NumericChangeId;
const file_name = 'index.php';
const file_contents = '<?php';
const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
element._cache.set(
`/changes/${change_num}/edit/${file_name}`,
{} as unknown as ParsedJSON
);
await element.saveChangeEdit(change_num, file_name, file_contents);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test~1/edit/' + file_name
);
assert.equal(fetchStub.lastCall.args[0].fetchOptions?.body, file_contents);
});
test('putChangeCommitMessage', async () => {
element._projectLookup = {1: Promise.resolve('test' as RepoName)};
const change_num = 1 as NumericChangeId;
const message = 'this is a commit message';
const committer_email = 'test@example.com';
const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
element._cache.set(
`/changes/${change_num}/message`,
{} as unknown as ParsedJSON
);
await element.putChangeCommitMessage(change_num, message, committer_email);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(fetchStub.lastCall.args[0].url, '/changes/test~1/message');
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{
message,
committer_email,
}
);
});
test('updateIdentityInChangeEdit', async () => {
element._projectLookup = {1: Promise.resolve('test' as RepoName)};
const change_num = 1 as NumericChangeId;
const name = 'user';
const email = 'user@example.com';
const type = 'AUTHOR';
const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
await element.updateIdentityInChangeEdit(change_num, name, email, type);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.PUT
);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test~1/edit:identity'
);
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{
email: 'user@example.com',
name: 'user',
type: 'AUTHOR',
}
);
});
test('deleteChangeCommitMessage', async () => {
element._projectLookup = {1: Promise.resolve('test' as RepoName)};
const change_num = 1 as NumericChangeId;
const messageId = 'abc' as ChangeMessageId;
const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
await element.deleteChangeCommitMessage(change_num, messageId);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.DELETE
);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test~1/messages/abc'
);
});
test('startWorkInProgress', async () => {
element.addRepoNameToCache(42 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response());
const urlSpy = sinon.spy(element, '_changeBaseURL');
await element.startWorkInProgress(42 as NumericChangeId);
assert.isTrue(fetchStub.calledOnce);
assert.isTrue(urlSpy.calledOnce);
assert.equal(urlSpy.lastCall.args[0], 42 as NumericChangeId);
assert.isNotOk(urlSpy.lastCall.args[1]);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.POST
);
assert.isTrue(fetchStub.lastCall.args[0].url.endsWith('/wip'));
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{}
);
await element.startWorkInProgress(42 as NumericChangeId, 'revising...');
assert.isTrue(fetchStub.calledTwice);
assert.equal(urlSpy.lastCall.args[0], 42 as NumericChangeId);
assert.isNotOk(urlSpy.lastCall.args[1]);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.POST
);
assert.isTrue(fetchStub.lastCall.args[0].url.endsWith('/wip'));
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{
message: 'revising...',
}
);
});
test('deleteComment', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const comment = createComment();
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves(comment as unknown as ParsedJSON);
const response = await element.deleteComment(
123 as NumericChangeId,
1 as PatchSetNum,
'01234' as UrlEncodedCommentId,
'removal reason'
);
assert.equal(response, comment);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].fetchOptions?.method,
HttpMethod.POST
);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/1/comments/01234/delete'
);
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{
reason: 'removal reason',
}
);
});
test('createRepo encodes name', async () => {
const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
await element.createRepo({name: 'x/y' as RepoName});
assert.isTrue(fetchStub.calledOnce);
assert.equal(fetchStub.lastCall.args[0].url, '/projects/x%2Fy');
});
test('queryChangeFiles', async () => {
element.addRepoNameToCache(42 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
await element.queryChangeFiles(42 as NumericChangeId, EDIT, 'test/path.js');
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~42/revisions/edit/files?q=test%2Fpath.js'
);
});
test('normal use', () => {
const defaultQuery = '';
assert.equal(
element._getReposUrl('test', 25).toString(),
[false, '/projects/?n=26&S=0&d=&m=test'].toString()
);
assert.equal(
element._getReposUrl(undefined, 25).toString(),
[false, `/projects/?n=26&S=0&d=&m=${defaultQuery}`].toString()
);
assert.equal(
element._getReposUrl('test', 25, 25).toString(),
[false, '/projects/?n=26&S=25&d=&m=test'].toString()
);
assert.equal(
element._getReposUrl('inname:test', 25, 25).toString(),
[true, '/projects/?n=26&S=25&query=inname%3Atest'].toString()
);
});
test('invalidateReposCache', () => {
const url = '/projects/?n=26&S=0&query=test';
element._cache.set(url, {} as unknown as ParsedJSON);
element.invalidateReposCache();
assert.isUndefined(element._sharedFetchPromises.get(url));
assert.isFalse(element._cache.has(url));
});
test('invalidateAccountsCache', () => {
const url = '/accounts/self/detail';
element._cache.set(url, {} as unknown as ParsedJSON);
element.invalidateAccountsCache();
assert.isUndefined(element._sharedFetchPromises.get(url));
assert.isFalse(element._cache.has(url));
});
suite('getRepos', () => {
const defaultQuery = '';
let fetchCacheJSONStub: sinon.SinonStub;
setup(() => {
fetchCacheJSONStub = sinon
.stub(element._restApiHelper, 'fetchCacheJSON')
.resolves([] as unknown as ParsedJSON);
});
test('normal use', () => {
element.getRepos('test', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=test'
);
element.getRepos(undefined, 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
`/projects/?n=26&S=0&d=&m=${defaultQuery}`
);
element.getRepos('test', 25, 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=25&d=&m=test'
);
});
test('with blank', () => {
element.getRepos('test/test', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=test%2Ftest'
);
});
test('with hyphen', () => {
element.getRepos('foo-bar', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=foo-bar'
);
});
test('with leading hyphen', () => {
element.getRepos('-bar', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=-bar'
);
});
test('with trailing hyphen', () => {
element.getRepos('foo-bar-', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=foo-bar-'
);
});
test('with underscore', () => {
element.getRepos('foo_bar', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=foo_bar'
);
});
test('with underscore', () => {
element.getRepos('foo_bar', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=foo_bar'
);
});
test('hyphen only', () => {
element.getRepos('-', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&d=&m=-'
);
});
test('using query', () => {
element.getRepos('description:project', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/?n=26&S=0&query=description%3Aproject'
);
});
});
test('_getGroupsUrl normal use', () => {
assert.equal(element._getGroupsUrl('test', 25), '/groups/?n=26&S=0&m=test');
assert.equal(element._getGroupsUrl('', 25), '/groups/?n=26&S=0');
assert.equal(
element._getGroupsUrl('test', 25, 25),
'/groups/?n=26&S=25&m=test'
);
});
test('invalidateGroupsCache', () => {
const url = '/groups/?n=26&S=0&m=test';
element._cache.set(url, {} as unknown as ParsedJSON);
element.invalidateGroupsCache();
assert.isUndefined(element._sharedFetchPromises.get(url));
assert.isFalse(element._cache.has(url));
});
suite('getGroups', () => {
let fetchCacheJSONStub: sinon.SinonStub;
setup(() => {
fetchCacheJSONStub = sinon.stub(element._restApiHelper, 'fetchCacheJSON');
});
test('normal use', () => {
element.getGroups('test', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=0&m=test'
);
element.getGroups('', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=0'
);
element.getGroups('test', 25, 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=25&m=test'
);
});
test('regex', () => {
element.getGroups('^test.*', 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=0&r=%5Etest.*'
);
element.getGroups('^test.*', 25, 25);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/groups/?n=26&S=25&r=%5Etest.*'
);
});
});
test('gerrit auth is used', () => {
const fetchStub = sinon.stub(authService, 'fetch').resolves();
element._restApiHelper.fetchJSON({url: 'foo'});
assert(fetchStub.called);
});
test('queryAccounts does not return fetchJSON', async () => {
const fetchJSONSpy = sinon.spy(element._restApiHelper, 'fetchJSON');
const accts = await element.queryAccounts('');
assert.isFalse(fetchJSONSpy.called);
assert.equal(accts!.length, 0);
});
test('fetchJSON gets called by queryAccounts', async () => {
const fetchJSONStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
await element.queryAccounts('own');
assert.deepEqual(fetchJSONStub.lastCall.args[0].params, {
q: '"own"',
o: 'DETAILS',
});
});
suite('getChangeDetail', () => {
let getConfigStub: sinon.SinonStub;
setup(() => {
getConfigStub = sinon
.stub(element, 'getConfig')
.resolves(createServerInfo());
});
suite('change detail options', () => {
let changeDetailStub: sinon.SinonStub;
setup(() => {
changeDetailStub = sinon
.stub(element, '_getChangeDetail')
.resolves({...createChange(), _number: 123 as NumericChangeId});
});
test('signed pushes disabled', async () => {
getConfigStub.resolves({
...createServerInfo(),
receive: {enable_signed_push: undefined},
});
const change = await element.getChangeDetail(123 as NumericChangeId);
assert.strictEqual(123, change!._number);
const options = changeDetailStub.firstCall.args[1];
assert.isNotOk(
parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
);
});
test('signed pushes enabled', async () => {
getConfigStub.resolves({
...createServerInfo(),
receive: {enable_signed_push: 'true'},
});
const change = await element.getChangeDetail(123 as NumericChangeId);
assert.strictEqual(123, change!._number);
const options = changeDetailStub.firstCall.args[1];
assert.ok(
parseInt(options, 16) & (1 << ListChangesOption.PUSH_CERTIFICATES)
);
});
});
test('GrReviewerUpdatesParser.parse is used', async () => {
element.addRepoNameToCache(42 as NumericChangeId, TEST_PROJECT_NAME);
const changeInfo = createParsedChange();
const parseStub = sinon
.stub(GrReviewerUpdatesParser, 'parse')
.resolves(changeInfo);
const result = await element.getChangeDetail(42 as NumericChangeId);
assert.isTrue(parseStub.calledOnce);
assert.equal(result, changeInfo);
});
test('_getChangeDetail passes params to ETags decorator', async () => {
const changeNum = 4321 as NumericChangeId;
element._projectLookup[changeNum] = Promise.resolve('test' as RepoName);
const expectedUrl = `${window.CANONICAL_PATH}/changes/test~4321/detail?O=516714`;
const optionsStub = sinon.stub(element._etags, 'getOptions');
const collectStub = sinon.stub(element._etags, 'collect');
sinon.stub(element._restApiHelper, 'fetch').resolves(
new Response(
makePrefixedJSON({
...createChange(),
_number: 123 as NumericChangeId,
})
)
);
await element._getChangeDetail(changeNum, '516714');
assert.isTrue(optionsStub.calledWithExactly(expectedUrl));
assert.equal(collectStub.lastCall.args[0], expectedUrl);
});
test('_getChangeDetail calls errFn on 500', async () => {
const errFn = sinon.stub();
sinon.stub(element, 'getChangeActionURL').resolves('');
sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(undefined, {status: 500}));
await element._getChangeDetail(123 as NumericChangeId, '516714', errFn);
assert.isTrue(errFn.called);
});
test('_getChangeDetail populates _projectLookup', async () => {
sinon.stub(element, 'getChangeActionURL').resolves('');
sinon.stub(element._restApiHelper, 'fetch').resolves(
new Response(')]}\'{"_number":1,"project":"test"}', {
status: 200,
})
);
await element._getChangeDetail(1 as NumericChangeId, '516714');
assert.equal(Object.keys(element._projectLookup).length, 1);
const project = await element._projectLookup[1];
assert.equal(project, 'test' as RepoName);
});
suite('_getChangeDetail ETag cache', () => {
let requestUrl: string;
let mockResponseSerial: string;
let collectSpy: sinon.SinonSpy;
setup(() => {
requestUrl = '/foo/bar';
const mockResponse = {foo: 'bar', baz: 42};
mockResponseSerial = makePrefixedJSON(mockResponse);
sinon.stub(element._restApiHelper, 'urlWithParams').returns(requestUrl);
sinon.stub(element, 'getChangeActionURL').resolves(requestUrl);
collectSpy = sinon.spy(element._etags, 'collect');
});
test('contributes to cache', async () => {
const getPayloadSpy = sinon.spy(element._etags, 'getCachedPayload');
sinon.stub(element._restApiHelper, 'fetch').resolves(
new Response(mockResponseSerial, {
status: 200,
})
);
await element._getChangeDetail(123 as NumericChangeId, '516714');
assert.isFalse(getPayloadSpy.called);
assert.isTrue(collectSpy.calledOnce);
const cachedResponse = element._etags.getCachedPayload(requestUrl);
assert.equal(cachedResponse, mockResponseSerial);
});
test('uses cache on HTTP 304', async () => {
const getPayloadStub = sinon.stub(element._etags, 'getCachedPayload');
getPayloadStub.returns(mockResponseSerial);
sinon.stub(element._restApiHelper, 'fetch').resolves(
new Response(undefined, {
status: 304,
})
);
await element._getChangeDetail(123 as NumericChangeId, '');
assert.isFalse(collectSpy.called);
assert.isTrue(getPayloadStub.calledOnce);
});
});
});
test('addRepoNameToCache', async () => {
element.addRepoNameToCache(555 as NumericChangeId, 'project' as RepoName);
const project = await element.getRepoName(555 as NumericChangeId);
assert.deepEqual(project, 'project' as RepoName);
});
suite('getRepoName', () => {
const changeNum = 555 as NumericChangeId;
const repo = 'test-repo' as RepoName;
test('getChange fails to yield a project', async () => {
const promise = mockPromise<undefined>();
sinon.stub(element, 'getChange').returns(promise);
const projectLookup = element.getRepoName(changeNum);
promise.resolve(undefined);
const err: Error = await assertFails(projectLookup);
assert.equal(
err.message,
'Failed to lookup the repo for change number 555'
);
});
test('getChange succeeds with project', async () => {
const promise = mockPromise<undefined | ChangeInfo>();
sinon.stub(element, 'getChange').returns(promise);
const projectLookup = element.getRepoName(changeNum);
promise.resolve({...createChange(), project: repo});
assert.equal(await projectLookup, repo);
assert.deepEqual(element._projectLookup, {'555': projectLookup});
});
test('getChange fails, but a addRepoNameToCache() call is used as fallback', async () => {
const promise = mockPromise<undefined>();
sinon.stub(element, 'getChange').returns(promise);
const projectLookup = element.getRepoName(changeNum);
element.addRepoNameToCache(changeNum, repo);
promise.resolve(undefined);
assert.equal(await projectLookup, repo);
});
});
suite('getChanges populates _projectLookup', () => {
test('multiple queries', async () => {
sinon.stub(element._restApiHelper, 'fetchJSON').resolves([
[
{_number: 1, project: 'test'},
{_number: 2, project: 'test'},
],
[{_number: 3, project: 'test/test'}],
] as unknown as ParsedJSON);
// When query instanceof Array, fetchJSON returns
// Array<Array<Object>>.
await element.getChangesForMultipleQueries(undefined, []);
assert.equal(Object.keys(element._projectLookup).length, 3);
const project1 = await element.getRepoName(1 as NumericChangeId);
assert.equal(project1, 'test' as RepoName);
const project2 = await element.getRepoName(2 as NumericChangeId);
assert.equal(project2, 'test' as RepoName);
const project3 = await element.getRepoName(3 as NumericChangeId);
assert.equal(project3, 'test/test' as RepoName);
});
test('no query', async () => {
sinon.stub(element._restApiHelper, 'fetchJSON').resolves([
{_number: 1, project: 'test'},
{_number: 2, project: 'test'},
{_number: 3, project: 'test/test'},
] as unknown as ParsedJSON);
// When query !instanceof Array, fetchJSON returns Array<Object>.
await element.getChanges();
assert.equal(Object.keys(element._projectLookup).length, 3);
const project1 = await element.getRepoName(1 as NumericChangeId);
assert.equal(project1, 'test' as RepoName);
const project2 = await element.getRepoName(2 as NumericChangeId);
assert.equal(project2, 'test' as RepoName);
const project3 = await element.getRepoName(3 as NumericChangeId);
assert.equal(project3, 'test/test' as RepoName);
});
});
test('getDetailedChangesWithActions', async () => {
const c1 = createChange();
c1._number = 1 as NumericChangeId;
const c2 = createChange();
c2._number = 2 as NumericChangeId;
const getChangesStub = sinon
.stub(element, 'getChanges')
.callsFake((changesPerPage, query, offset, options) => {
assert.isUndefined(changesPerPage);
assert.strictEqual(query, 'change:1 OR change:2');
assert.isUndefined(offset);
assert.strictEqual(options, EXPECTED_QUERY_OPTIONS);
return Promise.resolve([]);
});
await element.getDetailedChangesWithActions([c1._number, c2._number]);
assert.isTrue(getChangesStub.calledOnce);
});
test('setChangeTopic', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(makePrefixedJSON('foo-bar')));
await element.setChangeTopic(123 as NumericChangeId, 'foo-bar');
assert.isTrue(fetchStub.calledOnce);
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{topic: 'foo-bar'}
);
});
test('setChangeHashtag', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon.stub(element._restApiHelper, 'fetchJSON');
await element.setChangeHashtag(123 as NumericChangeId, {
add: ['foo-bar' as Hashtag],
});
assert.isTrue(fetchStub.calledOnce);
assert.sameDeepMembers(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string).add!,
['foo-bar']
);
});
test('generateAccountHttpPassword', async () => {
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
await element.generateAccountHttpPassword();
assert.isTrue(fetchStub.calledOnce);
assert.deepEqual(
JSON.parse(fetchStub.lastCall.args[0].fetchOptions?.body as string),
{generate: true}
);
});
suite('getChangeFiles', () => {
test('patch only', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
const range = {basePatchNum: PARENT, patchNum: 2 as RevisionPatchSetNum};
await element.getChangeFiles(123 as NumericChangeId, range);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/2/files'
);
assert.isNotOk(fetchStub.lastCall.args[0].params);
});
test('simple range', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
const range = {
basePatchNum: 4 as BasePatchSetNum,
patchNum: 5 as RevisionPatchSetNum,
};
await element.getChangeFiles(123 as NumericChangeId, range);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/5/files'
);
assert.isOk(fetchStub.lastCall.args[0].params);
assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
});
test('parent index', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
const range = {
basePatchNum: -3 as BasePatchSetNum,
patchNum: 5 as RevisionPatchSetNum,
};
await element.getChangeFiles(123 as NumericChangeId, range);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/5/files'
);
assert.isOk(fetchStub.lastCall.args[0].params);
assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
});
});
suite('getDiff', () => {
test('patchOnly', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
await element.getDiff(
123 as NumericChangeId,
PARENT,
2 as PatchSetNum,
'foo/bar.baz'
);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/2/files/foo%2Fbar.baz/diff'
);
assert.isOk(fetchStub.lastCall.args[0].params);
assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
});
test('simple range', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
await element.getDiff(
123 as NumericChangeId,
4 as PatchSetNum,
5 as PatchSetNum,
'foo/bar.baz'
);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/5/files/foo%2Fbar.baz/diff'
);
assert.isOk(fetchStub.lastCall.args[0].params);
assert.isNotOk(fetchStub.lastCall.args[0].params!.parent);
assert.equal(fetchStub.lastCall.args[0].params!.base, 4);
});
test('parent index', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
const fetchStub = sinon
.stub(element._restApiHelper, 'fetchJSON')
.resolves();
await element.getDiff(
123 as NumericChangeId,
-3 as PatchSetNum,
5 as PatchSetNum,
'foo/bar.baz'
);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/5/files/foo%2Fbar.baz/diff'
);
assert.isOk(fetchStub.lastCall.args[0].params);
assert.isNotOk(fetchStub.lastCall.args[0].params!.base);
assert.equal(fetchStub.lastCall.args[0].params!.parent, 3);
});
});
test('getDashboard', () => {
const fetchCacheJSONStub = sinon.stub(
element._restApiHelper,
'fetchCacheJSON'
);
element.getDashboard(
'gerrit/project' as RepoName,
'default:main' as DashboardId
);
assert.isTrue(fetchCacheJSONStub.calledOnce);
assert.equal(
fetchCacheJSONStub.lastCall.args[0].url,
'/projects/gerrit%2Fproject/dashboards/default%3Amain'
);
});
test('getFileContent', async () => {
element.addRepoNameToCache(1 as NumericChangeId, TEST_PROJECT_NAME);
sinon.stub(element._restApiHelper, 'fetch').callsFake(() =>
Promise.resolve(
new Response(makePrefixedJSON('new content'), {
status: 200,
headers: {
'X-FYI-Content-Type': 'text/java',
},
})
)
);
const edit = await element.getFileContent(
1 as NumericChangeId,
'tst/path',
'EDIT' as PatchSetNum
);
assert.deepEqual(edit, {
content: 'new content',
type: 'text/java',
ok: true,
});
const normal = await element.getFileContent(
1 as NumericChangeId,
'tst/path',
'3' as PatchSetNum
);
assert.deepEqual(normal, {
content: 'new content',
type: 'text/java',
ok: true,
});
});
test('getFileContent suppresses 404s', async () => {
const res404 = new Response(undefined, {status: 404});
const res500 = new Response(undefined, {status: 500});
const spy = sinon.spy();
addListenerForTest(document, 'server-error', spy);
const authStub = sinon.stub(authService, 'fetch').resolves(res404);
sinon.stub(element, '_changeBaseURL').resolves('');
await element.getFileContent(
1 as NumericChangeId,
'tst/path',
1 as PatchSetNum
);
await waitEventLoop();
assert.isFalse(spy.called);
authStub.reset();
authStub.resolves(res500);
await element.getFileContent(
1 as NumericChangeId,
'tst/path',
1 as PatchSetNum
);
assert.isTrue(spy.called);
assert.notEqual(spy.lastCall.args[0].detail.response.status, 404);
});
test('getChangeFilesOrEditFiles is edit-sensitive', async () => {
const getChangeFilesStub = sinon
.stub(element, 'getChangeFiles')
.resolves({});
const getChangeEditFilesStub = sinon
.stub(element, 'getChangeEditFiles')
.resolves({files: {}});
await element.getChangeOrEditFiles(1 as NumericChangeId, {
basePatchNum: PARENT,
patchNum: EDIT,
});
assert.isTrue(getChangeEditFilesStub.calledOnce);
assert.isFalse(getChangeFilesStub.called);
await element.getChangeOrEditFiles(1 as NumericChangeId, {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
});
assert.isTrue(getChangeEditFilesStub.calledOnce);
assert.isTrue(getChangeFilesStub.calledOnce);
});
test('getChangeEdit not logged in returns undefined', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
sinon.stub(element, 'getLoggedIn').resolves(false);
const fetchSpy = sinon.spy(element._restApiHelper, 'fetch');
const edit = await element.getChangeEdit(123 as NumericChangeId);
assert.isUndefined(edit);
assert.isFalse(fetchSpy.called);
});
test('getChangeEdit no edit patchset returns undefined', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
sinon.stub(element, 'getLoggedIn').resolves(true);
sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(undefined, {status: 204}));
const edit = await element.getChangeEdit(123 as NumericChangeId);
assert.isUndefined(edit);
});
test('getChangeEdit returns edit patchset', async () => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
sinon.stub(element, 'getLoggedIn').resolves(true);
const expected = createEditInfo();
sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(makePrefixedJSON(expected)));
const edit = await element.getChangeEdit(123 as NumericChangeId);
assert.deepEqual(edit, expected);
});
test('ported comment errors do not trigger error dialog', () => {
const change = createChange();
const handler = sinon.stub();
addListenerForTest(document, 'server-error', handler);
sinon.stub(element._restApiHelper, 'fetchJSON').resolves({
ok: false,
} as unknown as ParsedJSON);
element.getPortedComments(change._number, CURRENT);
assert.isFalse(handler.called);
});
test('ported drafts are not requested user is not logged in', () => {
const change = createChange();
element.addRepoNameToCache(change._number, TEST_PROJECT_NAME);
sinon.stub(element, 'getLoggedIn').resolves(false);
const getChangeURLAndFetchStub = sinon.stub(
element._restApiHelper,
'fetchJSON'
);
element.getPortedDrafts(change._number, CURRENT);
assert.isFalse(getChangeURLAndFetchStub.called);
});
test('saveChangeStarred', async () => {
element.addRepoNameToCache(123 as NumericChangeId, 'test' as RepoName);
element.addRepoNameToCache(456 as NumericChangeId, 'test' as RepoName);
const fetchStub = sinon.stub(element._restApiHelper, 'fetch').resolves();
await element.saveChangeStarred(123 as NumericChangeId, true);
assert.isTrue(fetchStub.calledOnce);
assert.deepEqual(fetchStub.lastCall.args[0], {
fetchOptions: {method: HttpMethod.PUT},
url: '/accounts/self/starred.changes/test~123',
anonymizedUrl: '/accounts/self/starred.changes/*',
});
await element.saveChangeStarred(456 as NumericChangeId, false);
assert.isTrue(fetchStub.calledTwice);
assert.deepEqual(fetchStub.lastCall.args[0], {
fetchOptions: {method: HttpMethod.DELETE},
url: '/accounts/self/starred.changes/test~456',
anonymizedUrl: '/accounts/self/starred.changes/*',
});
});
suite('applyFixSuggestion', () => {
const fixReplacementInfo = createFixReplacementInfo();
let fetchStub: sinon.SinonStub;
setup(() => {
element.addRepoNameToCache(123 as NumericChangeId, TEST_PROJECT_NAME);
fetchStub = sinon
.stub(element._restApiHelper, 'fetch')
.resolves(new Response(makePrefixedJSON({})));
});
test('applyFixSuggestion without targetPatchNum', async () => {
await element.applyFixSuggestion(
123 as NumericChangeId,
1 as PatchSetNum,
[fixReplacementInfo]
);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/1/fix:apply'
);
const body = JSON.parse(fetchStub.lastCall.args[0].fetchOptions.body);
assert.isTrue(
Object.keys(body).length === 1 &&
body.fix_replacement_infos.length === 1
);
assert.deepEqual(body.fix_replacement_infos[0], fixReplacementInfo);
});
test('applyFixSuggestion with same patchNum and targetPatchNum', async () => {
const fixReplacementInfo = createFixReplacementInfo();
await element.applyFixSuggestion(
123 as NumericChangeId,
1 as PatchSetNum,
[fixReplacementInfo],
1 as PatchSetNum
);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/1/fix:apply'
);
const body = JSON.parse(fetchStub.lastCall.args[0].fetchOptions.body);
assert.isTrue(Object.keys(body).length === 1);
assert.deepEqual(body.fix_replacement_infos[0], fixReplacementInfo);
});
test('applyFixSuggestion with targetPatchNum', async () => {
const fixReplacementInfo = createFixReplacementInfo();
await element.applyFixSuggestion(
123 as NumericChangeId,
1 as PatchSetNum,
[fixReplacementInfo],
2 as PatchSetNum
);
assert.isTrue(fetchStub.calledOnce);
assert.equal(
fetchStub.lastCall.args[0].url,
'/changes/test-project~123/revisions/2/fix:apply'
);
const body = JSON.parse(fetchStub.lastCall.args[0].fetchOptions.body);
assert.isTrue(Object.keys(body).length === 2);
assert.deepEqual(body.fix_replacement_infos[0], fixReplacementInfo);
assert.deepEqual(body.original_patchset_for_fix, 1);
});
});
});