blob: a4e0f09b4c9a165f48c5c04fc0f991a254a18145 [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
import './gr-repo';
import {GrRepo} from './gr-repo';
import {createChange} from '../../../test/test-data-generators';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {mockPromise} from '../../../test/test-utils';
import {
addListenerForTest,
queryAll,
queryAndAssert,
stubRestApi,
} from '../../../test/test-utils';
import {
createInheritedBoolean,
createServerInfo,
} from '../../../test/test-data-generators';
import {testResolver} from '../../../test/common-test-setup';
import {
ConfigInfo,
GitRef,
GroupId,
GroupName,
InheritedBooleanInfo,
MaxObjectSizeLimitInfo,
PluginParameterToConfigParameterInfoMap,
ProjectAccessInfo,
RepoAccessGroups,
RepoAccessInfoMap,
RepoName,
} from '../../../types/common';
import {
ConfigParameterInfoType,
InheritedBooleanInfoConfiguredValue,
PermissionAction,
RepoState,
SubmitType,
} from '../../../constants/constants';
import {
createConfig,
createDownloadSchemes,
} from '../../../test/test-data-generators';
import {PageErrorEvent} from '../../../types/events';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrSelect} from '../../shared/gr-select/gr-select';
import {GrSuggestionTextarea} from '../../shared/gr-suggestion-textarea/gr-suggestion-textarea';
import {IronInputElement} from '@polymer/iron-input/iron-input';
import {fixture, html, assert} from '@open-wc/testing';
import {getAppContext} from '../../../services/app-context';
import {KnownExperimentId} from '../../../services/flags/flags';
import {ChangeInfo} from '../../../api/rest-api';
suite('gr-repo tests', () => {
let element: GrRepo;
let loggedInStub: sinon.SinonStub;
let repoStub: sinon.SinonStub;
const repoConf: ConfigInfo = {
description: 'Access inherited by all other repositories.',
use_contributor_agreements: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
use_content_merge: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
use_signed_off_by: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
create_new_change_for_all_not_in_target: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
require_change_id: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
enable_signed_push: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
require_signed_push: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
reject_implicit_merges: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
match_author_to_committer_date: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
reject_empty_commit: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
enable_reviewer_by_email: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
private_by_default: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
work_in_progress_by_default: {
value: false,
configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
},
max_object_size_limit: {},
commentlinks: {},
submit_type: SubmitType.MERGE_IF_NECESSARY,
default_submit_type: {
value: SubmitType.MERGE_IF_NECESSARY,
configured_value: SubmitType.INHERIT,
inherited_value: SubmitType.MERGE_IF_NECESSARY,
},
};
const REPO = 'test-repo';
const SCHEMES = {
...createDownloadSchemes(),
http: {
url: 'test',
is_auth_required: false,
is_auth_supported: false,
commands: 'test',
clone_commands: {clone: 'test'},
},
repo: {
url: 'test',
is_auth_required: false,
is_auth_supported: false,
commands: 'test',
clone_commands: {clone: 'test'},
},
ssh: {
url: 'test',
is_auth_required: false,
is_auth_supported: false,
commands: 'test',
clone_commands: {clone: 'test'},
},
};
function getFormFields() {
const selects = Array.from(queryAll(element, 'select'));
const textareas = Array.from(queryAll(element, 'iron-autogrow-textarea'));
const inputs = Array.from(queryAll(element, 'input'));
return inputs.concat(textareas).concat(selects);
}
setup(async () => {
loggedInStub = stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
repoStub = stubRestApi('getProjectConfig').returns(
Promise.resolve(repoConf)
);
element = await fixture(html`<gr-repo></gr-repo>`);
});
test('render', async () => {
element.repo = REPO as RepoName;
await element.loadRepo();
await element.updateComplete;
// prettier and shadowDom assert do not agree about span.title wrapping
assert.shadowDom.equal(
element,
/* prettier-ignore */ /* HTML */ `
<div class="gr-form-styles main read-only">
<div class="info">
<h1 class="heading-1" id="Title">test-repo</h1>
<hr />
<div>
<a href="">
<gr-button
aria-disabled="true"
disabled=""
link=""
role="button"
tabindex="-1"
>
Browse
</gr-button>
</a>
<a href="/q/project:test-repo">
<gr-button
aria-disabled="false"
link=""
role="button"
tabindex="0"
>
View Changes
</gr-button>
</a>
</div>
</div>
<div id="loadedContent">
<h2 class="heading-2" id="configurations">Configurations</h2>
<div id="form">
<fieldset>
<h3 class="heading-3" id="Description">Description</h3>
<fieldset>
<gr-suggestion-textarea
autocomplete="on"
class="description monospace"
disabled=""
id="descriptionInput"
monospace=""
placeholder="<Insert repo description here>"
rows="4"
>
</gr-suggestion-textarea>
</fieldset>
<h3 class="heading-3" id="Options">Repository Options</h3>
<fieldset id="options">
<section>
<span class="title"> State </span>
<span class="value">
<gr-select id="stateSelect">
<select disabled="">
<option value="ACTIVE">Active</option>
<option value="READ_ONLY">Read Only</option>
<option value="HIDDEN">Hidden</option>
</select>
</gr-select>
</span>
</section>
<section>
<span class="title"> Submit type </span>
<span class="value">
<gr-select id="submitTypeSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title"> Allow content merges </span>
<span class="value">
<gr-select id="contentMergeSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title">
Create a new change for every commit not in the target branch
</span>
<span class="value">
<gr-select id="newChangeSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title">
Require Change-Id in commit message
</span>
<span class="value">
<gr-select id="requireChangeIdSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section
class="repositorySettings showConfig"
id="enableSignedPushSettings"
>
<span class="title"> Enable signed push </span>
<span class="value">
<gr-select id="enableSignedPush">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section
class="repositorySettings showConfig"
id="requireSignedPushSettings"
>
<span class="title"> Require signed push </span>
<span class="value">
<gr-select id="requireSignedPush">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title">
Reject implicit merges when changes are pushed for review
</span>
<span class="value">
<gr-select id="rejectImplicitMergesSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title">
Enable adding unregistered users as reviewers and CCs on changes
</span>
<span class="value">
<gr-select id="unRegisteredCcSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title">
Set all new changes private by default
</span>
<span class="value">
<gr-select id="setAllnewChangesPrivateByDefaultSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title">
Set new changes to "work in progress" by default
</span>
<span class="value">
<gr-select
id="setAllNewChangesWorkInProgressByDefaultSelect"
>
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title"> Maximum Git object size limit </span>
<span class="value">
<iron-input id="maxGitObjSizeIronInput">
<input disabled="" id="maxGitObjSizeInput" type="text" />
</iron-input>
</span>
</section>
<section>
<span class="title">
Match authored date with committer date upon submit
</span>
<span class="value">
<gr-select id="matchAuthoredDateWithCommitterDateSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title"> Reject empty commit upon submit </span>
<span class="value">
<gr-select id="rejectEmptyCommitSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
</fieldset>
<h3 class="heading-3" id="Options">Contributor Agreements</h3>
<fieldset id="agreements">
<section>
<span class="title">
Require a valid contributor agreement to upload
</span>
<span class="value">
<gr-select id="contributorAgreementSelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
<section>
<span class="title">
Require Signed-off-by in commit message
</span>
<span class="value">
<gr-select id="useSignedOffBySelect">
<select disabled=""></select>
</gr-select>
</span>
</section>
</fieldset>
<gr-button
id="saveBtn"
aria-disabled="true"
disabled=""
role="button"
tabindex="-1"
>
Save changes
</gr-button>
<gr-button
id="saveReviewBtn"
aria-disabled="true"
disabled=""
hidden=""
role="button"
tabindex="-1"
>
Save for review
</gr-button>
</fieldset>
<gr-endpoint-decorator name="repo-config">
<gr-endpoint-param name="repoName"> </gr-endpoint-param>
<gr-endpoint-param name="readOnly"> </gr-endpoint-param>
</gr-endpoint-decorator>
</div>
</div>
</div>
`,
{ignoreTags: ['option']}
);
});
test('render loading', async () => {
element.repo = REPO as RepoName;
element.loading = true;
await element.updateComplete;
// prettier and shadowDom assert do not agree about span.title wrapping
assert.shadowDom.equal(
element,
/* prettier-ignore */ /* HTML */ `
<div class="gr-form-styles main read-only">
<div class="info">
<h1 class="heading-1" id="Title">test-repo</h1>
<hr />
<div>
<a href="">
<gr-button
aria-disabled="true"
disabled=""
link=""
role="button"
tabindex="-1"
>
Browse
</gr-button>
</a>
<a href="/q/project:test-repo">
<gr-button
aria-disabled="false"
link=""
role="button"
tabindex="0"
>
View Changes
</gr-button>
</a>
</div>
</div>
<div id="loading">Loading...</div>
</div>
`,
{ignoreTags: ['option']}
);
});
test('_computePluginData', async () => {
element.repoConfig = {
...createConfig(),
plugin_config: {},
};
await element.updateComplete;
assert.deepEqual(element.computePluginData(), []);
element.repoConfig.plugin_config = {
'test-plugin': {
test: {display_name: 'test plugin', type: 'STRING'},
} as PluginParameterToConfigParameterInfoMap,
};
await element.updateComplete;
assert.deepEqual(element.computePluginData(), [
{
name: 'test-plugin',
config: {
test: {
display_name: 'test plugin',
type: 'STRING' as ConfigParameterInfoType,
},
},
},
]);
});
test('handlePluginConfigChanged', async () => {
const requestUpdateStub = sinon.stub(element, 'requestUpdate');
element.repoConfig = {
...createConfig(),
plugin_config: {},
};
element.handlePluginConfigChanged({
detail: {
name: 'test',
config: {
test: {display_name: 'test plugin', type: 'STRING'},
} as PluginParameterToConfigParameterInfoMap,
},
});
await element.updateComplete;
assert.deepEqual(element.repoConfig.plugin_config!.test, {
test: {display_name: 'test plugin', type: 'STRING'},
} as PluginParameterToConfigParameterInfoMap);
assert.isTrue(requestUpdateStub.called);
});
test('render download commands', async () => {
element.repo = REPO as RepoName;
await element.loadRepo();
element.schemesObj = SCHEMES;
await element.updateComplete;
const content = queryAndAssert<HTMLDivElement>(element, '#downloadContent');
assert.dom.equal(
content,
/* HTML */ `
<div id="downloadContent">
<h2 class="heading-2" id="download">Download</h2>
<fieldset>
<gr-download-commands id="downloadCommands"></gr-download-commands>
</fieldset>
</div>
`
);
});
test('form defaults to read only', () => {
assert.isTrue(element.readOnly);
});
test('form defaults to read only when not logged in', async () => {
element.repo = REPO as RepoName;
await element.loadRepo();
assert.isTrue(element.readOnly);
});
test('form defaults to read only when logged in and not admin', async () => {
element.repo = REPO as RepoName;
stubRestApi('getRepoAccess').callsFake(() =>
Promise.resolve({
'test-repo': {
revision: 'xxxx',
local: {
'refs/*': {
permissions: {
owner: {rules: {xxx: {action: 'ALLOW', force: false}}},
},
},
},
owner_of: ['refs/*'] as GitRef[],
groups: {
xxxx: {
id: 'xxxx' as GroupId,
url: 'test',
name: 'test' as GroupName,
},
} as RepoAccessGroups,
config_web_links: [{name: 'gitiles', url: 'test'}],
},
} as RepoAccessInfoMap)
);
await element.loadRepo();
assert.isTrue(element.readOnly);
});
test('all form elements are disabled when not admin', async () => {
element.repo = REPO as RepoName;
await element.loadRepo();
await element.updateComplete;
const formFields = getFormFields();
for (const field of formFields) {
assert.isTrue(field.hasAttribute('disabled'));
}
});
test('formatBooleanSelect', () => {
let item: InheritedBooleanInfo = {
...createInheritedBoolean(true),
inherited_value: true,
};
assert.deepEqual(element.formatBooleanSelect(item), [
{
label: 'Inherit (true)',
value: 'INHERIT',
},
{
label: 'True',
value: 'TRUE',
},
{
label: 'False',
value: 'FALSE',
},
]);
item = {...createInheritedBoolean(false), inherited_value: false};
assert.deepEqual(element.formatBooleanSelect(item), [
{
label: 'Inherit (false)',
value: 'INHERIT',
},
{
label: 'True',
value: 'TRUE',
},
{
label: 'False',
value: 'FALSE',
},
]);
// For items without inherited values
item = createInheritedBoolean(false);
assert.deepEqual(element.formatBooleanSelect(item), [
{
label: 'Inherit',
value: 'INHERIT',
},
{
label: 'True',
value: 'TRUE',
},
{
label: 'False',
value: 'FALSE',
},
]);
});
test('fires page-error', async () => {
repoStub.restore();
element.repo = 'test' as RepoName;
const pageErrorFired = mockPromise();
const response = {...new Response(), status: 404};
stubRestApi('getProjectConfig').callsFake((_, errFn) => {
if (errFn !== undefined) {
errFn(response);
}
return Promise.resolve(undefined);
});
addListenerForTest(document, 'page-error', e => {
assert.deepEqual((e as PageErrorEvent).detail.response, response);
pageErrorFired.resolve();
});
element.loadRepo();
await pageErrorFired;
});
suite('admin', () => {
const testRepoAccess: ProjectAccessInfo = {
revision: 'xxxx',
local: {
'refs/*': {
permissions: {
owner: {
rules: {xxx: {action: PermissionAction.ALLOW, force: false}},
},
},
},
},
is_owner: true,
owner_of: ['refs/*'] as GitRef[],
groups: {
xxxx: {
id: 'xxxx' as GroupId,
url: 'test',
name: 'test' as GroupName,
},
} as RepoAccessGroups,
config_web_links: [{name: 'gitiles', url: 'test'}],
};
let getRepoAccessStub: sinon.SinonStub;
setup(() => {
element.repo = REPO as RepoName;
loggedInStub.returns(Promise.resolve(true));
getRepoAccessStub = stubRestApi('getRepoAccess').callsFake(() =>
Promise.resolve({
'test-repo': testRepoAccess,
} as RepoAccessInfoMap)
);
});
test('all form elements are enabled', async () => {
await element.loadRepo();
await element.updateComplete;
const formFields = getFormFields();
for (const field of formFields) {
assert.isFalse(field.hasAttribute('disabled'));
}
assert.isFalse(element.loading);
});
test('state gets set correctly', async () => {
await element.loadRepo();
assert.equal(element.repoConfig!.state, RepoState.ACTIVE);
assert.equal(
queryAndAssert<GrSelect>(element, '#stateSelect').bindValue,
RepoState.ACTIVE
);
});
test('inherited submit type value is calculated correctly', async () => {
await element.loadRepo();
const sel = queryAndAssert<GrSelect>(element, '#submitTypeSelect');
assert.equal(sel.bindValue, 'INHERIT');
assert.equal(
sel.nativeSelect.options[0].text,
'Inherit (Merge if necessary)'
);
});
test('fields update and save correctly', async () => {
const configInputObj = {
description: 'new description',
use_contributor_agreements: InheritedBooleanInfoConfiguredValue.TRUE,
use_content_merge: InheritedBooleanInfoConfiguredValue.TRUE,
use_signed_off_by: InheritedBooleanInfoConfiguredValue.TRUE,
create_new_change_for_all_not_in_target:
InheritedBooleanInfoConfiguredValue.TRUE,
require_change_id: InheritedBooleanInfoConfiguredValue.TRUE,
enable_signed_push: InheritedBooleanInfoConfiguredValue.TRUE,
require_signed_push: InheritedBooleanInfoConfiguredValue.TRUE,
reject_implicit_merges: InheritedBooleanInfoConfiguredValue.TRUE,
private_by_default: InheritedBooleanInfoConfiguredValue.TRUE,
work_in_progress_by_default: InheritedBooleanInfoConfiguredValue.TRUE,
match_author_to_committer_date:
InheritedBooleanInfoConfiguredValue.TRUE,
reject_empty_commit: InheritedBooleanInfoConfiguredValue.TRUE,
max_object_size_limit: '10' as MaxObjectSizeLimitInfo,
submit_type: SubmitType.FAST_FORWARD_ONLY,
state: RepoState.READ_ONLY,
enable_reviewer_by_email: InheritedBooleanInfoConfiguredValue.TRUE,
};
const saveStub = stubRestApi('saveRepoConfig').callsFake(() =>
Promise.resolve(new Response())
);
await element.loadRepo();
const button = queryAndAssert<GrButton>(element, 'gr-button#saveBtn');
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(
queryAndAssert<HTMLHeadingElement>(
element,
'#Title'
).classList.contains('edited')
);
queryAndAssert<GrSuggestionTextarea>(element, '#descriptionInput').text =
configInputObj.description;
queryAndAssert<GrSelect>(element, '#stateSelect').bindValue =
configInputObj.state;
queryAndAssert<GrSelect>(element, '#submitTypeSelect').bindValue =
configInputObj.submit_type;
queryAndAssert<GrSelect>(element, '#contentMergeSelect').bindValue =
configInputObj.use_content_merge;
queryAndAssert<GrSelect>(element, '#newChangeSelect').bindValue =
configInputObj.create_new_change_for_all_not_in_target;
queryAndAssert<GrSelect>(element, '#requireChangeIdSelect').bindValue =
configInputObj.require_change_id;
queryAndAssert<GrSelect>(element, '#enableSignedPush').bindValue =
configInputObj.enable_signed_push;
queryAndAssert<GrSelect>(element, '#requireSignedPush').bindValue =
configInputObj.require_signed_push;
queryAndAssert<GrSelect>(
element,
'#rejectImplicitMergesSelect'
).bindValue = configInputObj.reject_implicit_merges;
queryAndAssert<GrSelect>(
element,
'#setAllnewChangesPrivateByDefaultSelect'
).bindValue = configInputObj.private_by_default;
queryAndAssert<GrSelect>(
element,
'#setAllNewChangesWorkInProgressByDefaultSelect'
).bindValue = configInputObj.work_in_progress_by_default;
queryAndAssert<GrSelect>(
element,
'#matchAuthoredDateWithCommitterDateSelect'
).bindValue = configInputObj.match_author_to_committer_date;
queryAndAssert<IronInputElement>(
element,
'#maxGitObjSizeIronInput'
).bindValue = String(configInputObj.max_object_size_limit);
queryAndAssert<GrSelect>(
element,
'#contributorAgreementSelect'
).bindValue = configInputObj.use_contributor_agreements;
queryAndAssert<GrSelect>(element, '#useSignedOffBySelect').bindValue =
configInputObj.use_signed_off_by;
queryAndAssert<GrSelect>(element, '#rejectEmptyCommitSelect').bindValue =
configInputObj.reject_empty_commit;
queryAndAssert<GrSelect>(element, '#unRegisteredCcSelect').bindValue =
configInputObj.enable_reviewer_by_email;
await element.updateComplete;
assert.isFalse(button.hasAttribute('disabled'));
assert.isTrue(
queryAndAssert<HTMLHeadingElement>(
element,
'#configurations'
).classList.contains('edited')
);
const formattedObj = element.formatRepoConfigForSave(element.repoConfig);
assert.deepEqual(formattedObj, configInputObj);
await element.handleSaveRepoConfig();
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(
queryAndAssert<HTMLHeadingElement>(
element,
'#Title'
).classList.contains('edited')
);
assert.isTrue(
saveStub.lastCall.calledWithExactly(REPO as RepoName, configInputObj)
);
});
test('saveReviewBtn visible when experiment is enabled', async () => {
const flagsService = getAppContext().flagsService;
sinon
.stub(flagsService, 'isEnabled')
.callsFake(
id => id === KnownExperimentId.SAVE_PROJECT_CONFIG_FOR_REVIEW
);
await element.loadRepo();
await element.updateComplete;
const button = queryAndAssert<GrButton>(
element,
'gr-button#saveReviewBtn'
);
assert.isFalse(button.hasAttribute('hidden'));
});
test('saveBtn remains disabled when require_change_for_config_update is set', async () => {
const flagsService = getAppContext().flagsService;
sinon
.stub(flagsService, 'isEnabled')
.callsFake(
id => id === KnownExperimentId.SAVE_PROJECT_CONFIG_FOR_REVIEW
);
getRepoAccessStub.callsFake(() =>
Promise.resolve({
'test-repo': {
...testRepoAccess,
require_change_for_config_update: true,
},
} as RepoAccessInfoMap)
);
await element.loadRepo();
await element.updateComplete;
const button = queryAndAssert<GrButton>(element, 'gr-button#saveBtn');
assert.isTrue(button.hasAttribute('disabled'));
});
test('saveReviewBtn', async () => {
const flagsService = getAppContext().flagsService;
sinon
.stub(flagsService, 'isEnabled')
.callsFake(
id => id === KnownExperimentId.SAVE_PROJECT_CONFIG_FOR_REVIEW
);
let resolver: (value: ChangeInfo | PromiseLike<ChangeInfo>) => void;
const saveForReviewStub = stubRestApi('saveRepoConfigForReview').returns(
new Promise(r => (resolver = r))
);
resolver!(createChange());
const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
await element.loadRepo();
await element.updateComplete;
const input = queryAndAssert<GrSuggestionTextarea>(
element,
'#descriptionInput'
);
input.text = 'New description';
await input.updateComplete;
await element.updateComplete;
const button = queryAndAssert<GrButton>(element, 'gr-button#saveBtn');
assert.isFalse(button.hasAttribute('disabled'));
queryAndAssert<GrButton>(element, 'gr-button#saveReviewBtn').click();
await element.updateComplete;
assert.isTrue(saveForReviewStub.called);
assert.isTrue(setUrlStub.called);
assert.isTrue(
setUrlStub.lastCall.args?.[0]?.includes(`${createChange()._number}`)
);
});
});
});