blob: c321abff94bb3a43ea28957dff50a679eb6bca76 [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as sinon from 'sinon';
import '../../../test/common-test-setup';
import './gr-repo-access';
import {GrRepoAccess} from './gr-repo-access';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {toSortedPermissionsArray} from '../../../utils/access-util';
import {
addListenerForTest,
mockPromise,
queryAll,
queryAndAssert,
stubRestApi,
} from '../../../test/test-utils';
import {
ChangeInfo,
GitRef,
RepoName,
UrlEncodedRepoName,
} from '../../../types/common';
import {PermissionAction} from '../../../constants/constants';
import {AutocompleteCommitEvent, PageErrorEvent} from '../../../types/events';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GrAccessSection} from '../gr-access-section/gr-access-section';
import {GrPermission} from '../gr-permission/gr-permission';
import {createChange} from '../../../test/test-data-generators';
import {fixture, html, assert} from '@open-wc/testing';
import {testResolver} from '../../../test/common-test-setup';
suite('gr-repo-access tests', () => {
let element: GrRepoAccess;
let repoStub: sinon.SinonStub;
const accessRes = {
local: {
'refs/*': {
permissions: {
owner: {
rules: {
234: {action: PermissionAction.ALLOW},
123: {action: PermissionAction.DENY},
},
},
read: {
rules: {
234: {action: PermissionAction.ALLOW},
},
},
},
},
},
groups: {
Administrators: {
name: 'Administrators',
},
Maintainers: {
name: 'Maintainers',
},
},
config_web_links: [
{
name: 'gitiles',
target: '_blank',
url: 'https://my/site/+log/123/project.config',
},
],
can_upload: true,
};
const accessRes2 = {
local: {
GLOBAL_CAPABILITIES: {
permissions: {
accessDatabase: {
rules: {
group1: {
action: PermissionAction.ALLOW,
},
},
},
},
},
},
};
const repoRes = {
id: '' as UrlEncodedRepoName,
labels: {
'Code-Review': {
values: {
'0': 'No score',
'-1': 'I would prefer this is not submitted as is',
'-2': 'This shall not be submitted',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
default_value: 0,
},
},
};
const capabilitiesRes = {
accessDatabase: {
id: 'accessDatabase',
name: 'Access Database',
},
createAccount: {
id: 'createAccount',
name: 'Create Account',
},
};
setup(async () => {
element = await fixture<GrRepoAccess>(html`
<gr-repo-access></gr-repo-access>
`);
stubRestApi('getAccount').returns(Promise.resolve(undefined));
repoStub = stubRestApi('getRepo').returns(Promise.resolve(repoRes));
element.loading = false;
element.ownerOf = [];
element.canUpload = false;
await element.updateComplete;
});
test('render', () => {
assert.shadowDom.equal(
element,
/* HTML */ `
<div class="main">
<div id="loading">Loading...</div>
<div id="loadedContent">
<h3 class="heading-3" id="inheritsFrom">
<span class="rightsText"> Rights Inherit From </span>
<a href="" id="inheritFromName" rel="noopener"> </a>
<gr-autocomplete id="editInheritFromInput"> </gr-autocomplete>
</h3>
<div class="weblinks">History:</div>
<div class="referenceContainer">
<gr-button
aria-disabled="false"
id="addReferenceBtn"
role="button"
tabindex="0"
>
Add Reference
</gr-button>
</div>
<div>
<gr-button
aria-disabled="false"
id="editBtn"
role="button"
tabindex="0"
>
Edit
</gr-button>
<gr-button
aria-disabled="true"
disabled=""
class="invisible"
id="saveBtn"
primary=""
role="button"
tabindex="-1"
>
Save
</gr-button>
<gr-button
aria-disabled="false"
class="invisible"
id="saveReviewBtn"
primary=""
role="button"
tabindex="0"
>
Save for review
</gr-button>
</div>
</div>
</div>
`
);
});
test('_repoChanged called when repo name changes', async () => {
const repoChangedStub = sinon.stub(element, '_repoChanged');
element.repo = 'New Repo' as RepoName;
await element.updateComplete;
assert.isTrue(repoChangedStub.called);
});
test('_repoChanged', async () => {
const accessStub = stubRestApi('getRepoAccessRights');
accessStub
.withArgs('New Repo' as RepoName)
.returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
accessStub
.withArgs('Another New Repo' as RepoName)
.returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
const capabilitiesStub = stubRestApi('getCapabilities');
capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
await element._repoChanged('New Repo' as RepoName);
assert.isTrue(accessStub.called);
assert.isTrue(capabilitiesStub.called);
assert.isTrue(repoStub.called);
assert.isNotOk(element.inheritsFrom);
assert.deepEqual(element.local, accessRes.local);
assert.deepEqual(
element.sections,
toSortedPermissionsArray(accessRes.local)
);
assert.deepEqual(element.labels, repoRes.labels);
assert.equal(
getComputedStyle(queryAndAssert<HTMLDivElement>(element, '.weblinks'))
.display,
'block'
);
await element._repoChanged('Another New Repo' as RepoName);
assert.deepEqual(
element.sections,
toSortedPermissionsArray(accessRes2.local)
);
assert.equal(
getComputedStyle(queryAndAssert<HTMLDivElement>(element, '.weblinks'))
.display,
'none'
);
});
test('_repoChanged when repo changes to undefined returns', async () => {
const capabilitiesRes = {
accessDatabase: {
id: 'accessDatabase',
name: 'Access Database',
},
};
const accessStub = stubRestApi('getRepoAccessRights').returns(
Promise.resolve(JSON.parse(JSON.stringify(accessRes2)))
);
const capabilitiesStub = stubRestApi('getCapabilities').returns(
Promise.resolve(capabilitiesRes)
);
await element._repoChanged();
assert.isFalse(accessStub.called);
assert.isFalse(capabilitiesStub.called);
assert.isFalse(repoStub.called);
});
test('computeParentHref', () => {
element.inheritsFrom!.name = 'test-repo' as RepoName;
assert.equal(element.computeParentHref(), '/admin/repos/test-repo,access');
});
test('computeMainClass', () => {
element.ownerOf = ['refs/*'] as GitRef[];
element.editing = false;
element.canUpload = false;
assert.equal(element.computeMainClass(), 'admin');
element.editing = true;
assert.equal(element.computeMainClass(), 'admin editing');
element.ownerOf = [];
element.editing = false;
assert.equal(element.computeMainClass(), '');
element.editing = true;
assert.equal(element.computeMainClass(), 'editing');
});
test('inherit section', async () => {
element.local = {};
element.ownerOf = [];
const computeParentHrefStub = sinon.stub(element, 'computeParentHref');
await element.updateComplete;
// Nothing should appear when no inherit from and not in edit mode.
assert.equal(
getComputedStyle(
queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom')
).display,
'none'
);
// When in edit mode, the autocomplete should appear.
element.editing = true;
// When editing, the autocomplete should still not be shown.
assert.equal(
getComputedStyle(
queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom')
).display,
'none'
);
element.editing = false;
element.inheritsFrom = {
id: '1234' as UrlEncodedRepoName,
name: 'another-repo' as RepoName,
};
await element.updateComplete;
// When there is a parent repo, the link should be displayed.
assert.notEqual(
getComputedStyle(
queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom')
).display,
'none'
);
assert.notEqual(
getComputedStyle(
queryAndAssert<HTMLAnchorElement>(element, '#inheritFromName')
).display,
'none'
);
assert.equal(
getComputedStyle(
queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
).display,
'none'
);
assert.isTrue(computeParentHrefStub.called);
element.editing = true;
await element.updateComplete;
// When editing, the autocomplete should be shown.
assert.notEqual(
getComputedStyle(
queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom')
).display,
'none'
);
assert.equal(
getComputedStyle(
queryAndAssert<HTMLAnchorElement>(element, '#inheritFromName')
).display,
'none'
);
assert.notEqual(
getComputedStyle(
queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
).display,
'none'
);
});
test('handleUpdateInheritFrom', async () => {
element.inheritFromFilter = 'foo bar baz' as RepoName;
await element.updateComplete;
element.handleUpdateInheritFrom({
detail: {value: 'abc+123'},
} as CustomEvent);
await element.updateComplete;
assert.isOk(element.inheritsFrom);
assert.equal(element.inheritsFrom!.id, 'abc+123');
assert.equal(element.inheritsFrom!.name, 'foo bar baz' as RepoName);
});
test('fires page-error', async () => {
const response = {status: 404} as Response;
stubRestApi('getRepoAccessRights').callsFake((_repoName, errFn) => {
if (errFn !== undefined) {
errFn(response);
}
return Promise.resolve(undefined);
});
const promise = mockPromise();
addListenerForTest(document, 'page-error', e => {
assert.deepEqual((e as PageErrorEvent).detail.response, response);
promise.resolve();
});
element.repo = 'test' as RepoName;
await promise;
});
suite('with defined sections', () => {
const testEditSaveCancelBtns = async (
shouldShowSave: boolean,
shouldShowSaveReview: boolean
) => {
// Edit button is visible and Save button is hidden.
assert.equal(
getComputedStyle(queryAndAssert<GrButton>(element, '#saveReviewBtn'))
.display,
'none'
);
assert.equal(
getComputedStyle(queryAndAssert<GrButton>(element, '#saveBtn')).display,
'none'
);
assert.notEqual(
getComputedStyle(queryAndAssert<GrButton>(element, '#editBtn')).display,
'none'
);
assert.equal(
queryAndAssert<GrButton>(element, '#editBtn').innerText,
'EDIT'
);
assert.equal(
getComputedStyle(
queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
).display,
'none'
);
element.inheritsFrom = {
id: 'test-project' as UrlEncodedRepoName,
};
await element.updateComplete;
assert.equal(
getComputedStyle(
queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
).display,
'none'
);
queryAndAssert<GrButton>(element, '#editBtn').click();
await element.updateComplete;
// Edit button changes to Cancel button, and Save button is visible but
// disabled.
assert.equal(
queryAndAssert<GrButton>(element, '#editBtn').innerText,
'CANCEL'
);
if (shouldShowSaveReview) {
assert.notEqual(
getComputedStyle(queryAndAssert<GrButton>(element, '#saveReviewBtn'))
.display,
'none'
);
assert.isTrue(
queryAndAssert<GrButton>(element, '#saveReviewBtn').disabled
);
}
if (shouldShowSave) {
assert.notEqual(
getComputedStyle(queryAndAssert<GrButton>(element, '#saveBtn'))
.display,
'none'
);
assert.isTrue(queryAndAssert<GrButton>(element, '#saveBtn').disabled);
}
assert.notEqual(
getComputedStyle(
queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
).display,
'none'
);
// Save button should be enabled after access is modified
element.dispatchEvent(
new CustomEvent('access-modified', {
composed: true,
bubbles: true,
})
);
if (shouldShowSaveReview) {
assert.isFalse(
queryAndAssert<GrButton>(element, '#saveReviewBtn').disabled
);
}
if (shouldShowSave) {
assert.isFalse(queryAndAssert<GrButton>(element, '#saveBtn').disabled);
}
};
setup(async () => {
// Create deep copies of these objects so the originals are not modified
// by any tests.
element.local = JSON.parse(JSON.stringify(accessRes.local));
element.ownerOf = [];
element.sections = toSortedPermissionsArray(element.local);
element.groups = JSON.parse(JSON.stringify(accessRes.groups));
element.capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
element.labels = JSON.parse(JSON.stringify(repoRes.labels));
await element.updateComplete;
});
test('removing an added section', async () => {
element.editing = true;
await element.updateComplete;
assert.equal(element.sections!.length, 1);
queryAndAssert<GrAccessSection>(
element,
'gr-access-section'
).dispatchEvent(
new CustomEvent('added-section-removed', {
composed: true,
bubbles: true,
})
);
await element.updateComplete;
assert.equal(element.sections!.length, 0);
});
test('button visibility for non ref owner', async () => {
assert.equal(
getComputedStyle(queryAndAssert<GrButton>(element, '#saveReviewBtn'))
.display,
'none'
);
assert.equal(
getComputedStyle(queryAndAssert<GrButton>(element, '#editBtn')).display,
'none'
);
});
test('button visibility for non ref owner with upload privilege', async () => {
element.canUpload = true;
await element.updateComplete;
testEditSaveCancelBtns(false, true);
});
test('button visibility for ref owner', async () => {
element.ownerOf = ['refs/for/*'] as GitRef[];
await element.updateComplete;
testEditSaveCancelBtns(true, false);
});
test('button visibility for ref owner and upload', async () => {
element.ownerOf = ['refs/for/*'] as GitRef[];
element.canUpload = true;
await element.updateComplete;
testEditSaveCancelBtns(true, false);
});
test('_handleAccessModified called with event fired', async () => {
const handleAccessModifiedSpy = sinon.spy(
element,
'_handleAccessModified'
);
element.dispatchEvent(
new CustomEvent('access-modified', {
composed: true,
bubbles: true,
})
);
await element.updateComplete;
assert.isTrue(handleAccessModifiedSpy.called);
});
test('_handleAccessModified called when parent changes', async () => {
element.inheritsFrom = {
id: 'test-project' as UrlEncodedRepoName,
};
await element.updateComplete;
queryAndAssert<GrAutocomplete>(
element,
'#editInheritFromInput'
).dispatchEvent(
new CustomEvent('commit', {
detail: {},
composed: true,
bubbles: true,
})
);
const handleAccessModifiedSpy = sinon.spy(
element,
'_handleAccessModified'
);
element.dispatchEvent(
new CustomEvent('access-modified', {
detail: {},
composed: true,
bubbles: true,
})
);
await element.updateComplete;
assert.isTrue(handleAccessModifiedSpy.called);
});
test('handleSaveForReview', async () => {
const saveStub = stubRestApi('setRepoAccessRightsForReview');
sinon.stub(element, 'computeAddAndRemove').returns({
add: {},
remove: {},
});
element.handleSaveForReview(new Event('test'));
await element.updateComplete;
assert.isFalse(saveStub.called);
});
test('recursivelyRemoveDeleted', () => {
const obj = {
'refs/*': {
permissions: {
owner: {
rules: {
234: {action: 'ALLOW'},
123: {action: 'DENY', deleted: true},
},
},
read: {
deleted: true,
rules: {
234: {action: 'ALLOW'},
},
},
},
},
};
const expectedResult = {
'refs/*': {
permissions: {
owner: {
rules: {
234: {action: 'ALLOW'},
},
},
},
},
};
element.recursivelyRemoveDeleted(obj);
assert.deepEqual(obj, expectedResult);
});
test('recursivelyUpdateAddRemoveObj on new added section', () => {
const obj = {
'refs/for/*': {
permissions: {
'label-Code-Review': {
rules: {
e798fed07afbc9173a587f876ef8760c78d240c1: {
min: -2,
max: 2,
action: 'ALLOW',
added: true,
},
},
added: true,
label: 'Code-Review',
},
'labelAs-Code-Review': {
rules: {
'ldap:gerritcodereview-eng': {
min: -2,
max: 2,
action: 'ALLOW',
added: true,
deleted: true,
},
},
added: true,
label: 'Code-Review',
},
},
added: true,
},
};
const expectedResult = {
add: {
'refs/for/*': {
permissions: {
'label-Code-Review': {
rules: {
e798fed07afbc9173a587f876ef8760c78d240c1: {
min: -2,
max: 2,
action: 'ALLOW',
added: true,
},
},
added: true,
label: 'Code-Review',
},
'labelAs-Code-Review': {
rules: {},
added: true,
label: 'Code-Review',
},
},
added: true,
},
},
remove: {},
};
const updateObj = {add: {}, remove: {}};
element.recursivelyUpdateAddRemoveObj(obj, updateObj);
assert.deepEqual(updateObj, expectedResult);
});
test('handleSaveForReview with no changes', () => {
assert.deepEqual(element.computeAddAndRemove(), {add: {}, remove: {}});
});
test('handleSaveForReview parent change', async () => {
element.inheritsFrom = {
id: 'test-project' as UrlEncodedRepoName,
};
element.originalInheritsFrom = {
id: 'test-project-original' as UrlEncodedRepoName,
};
await element.updateComplete;
assert.deepEqual(element.computeAddAndRemove(), {
parent: 'test-project',
add: {},
remove: {},
});
});
test('handleSaveForReview new parent with spaces', async () => {
element.inheritsFrom = {
id: 'spaces+in+project+name' as UrlEncodedRepoName,
};
element.originalInheritsFrom = {id: 'old-project' as UrlEncodedRepoName};
await element.updateComplete;
assert.deepEqual(element.computeAddAndRemove(), {
parent: 'spaces in project name',
add: {},
remove: {},
});
});
test('handleSaveForReview rules', async () => {
// Delete a rule.
element.local!['refs/*'].permissions.owner.rules[123].deleted = true;
await element.updateComplete;
let expectedInput = {
add: {},
remove: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {},
},
},
},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Undo deleting a rule.
delete element.local!['refs/*'].permissions.owner.rules[123].deleted;
// Modify a rule.
element.local!['refs/*'].permissions.owner.rules[123].modified = true;
await element.updateComplete;
expectedInput = {
add: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {action: 'DENY', modified: true},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {},
},
},
},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
});
test('computeAddAndRemove permissions', async () => {
// Add a new rule to a permission.
let expectedInput = {};
expectedInput = {
add: {
'refs/*': {
permissions: {
owner: {
rules: {
Maintainers: {
action: 'ALLOW',
added: true,
},
},
},
},
},
},
remove: {},
};
const grAccessSection = queryAndAssert<GrAccessSection>(
element,
'gr-access-section'
);
await queryAndAssert<GrPermission>(
grAccessSection,
'gr-permission'
).handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Remove the added rule.
delete element.local!['refs/*'].permissions.owner.rules.Maintainers;
// Delete a permission.
element.local!['refs/*'].permissions.owner.deleted = true;
await element.updateComplete;
expectedInput = {
add: {},
remove: {
'refs/*': {
permissions: {
owner: {rules: {}},
},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Undo delete permission.
delete element.local!['refs/*'].permissions.owner.deleted;
// Modify a permission.
element.local!['refs/*'].permissions.owner.modified = true;
await element.updateComplete;
expectedInput = {
add: {
'refs/*': {
permissions: {
owner: {
modified: true,
rules: {
234: {action: 'ALLOW'},
123: {action: 'DENY'},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {
owner: {rules: {}},
},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
});
test('computeAddAndRemove sections', async () => {
// Add a new permission to a section
let expectedInput = {};
expectedInput = {
add: {
'refs/*': {
permissions: {
'label-Code-Review': {
added: true,
rules: {},
label: 'Code-Review',
},
},
},
},
remove: {},
};
queryAndAssert<GrAccessSection>(
element,
'gr-access-section'
).handleAddPermission();
await element.updateComplete;
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Add a new rule to the new permission.
expectedInput = {
add: {
'refs/*': {
permissions: {
'label-Code-Review': {
added: true,
rules: {
Maintainers: {
min: -2,
max: 2,
action: 'ALLOW',
added: true,
},
},
label: 'Code-Review',
},
},
},
},
remove: {},
};
const grAccessSection = queryAndAssert<GrAccessSection>(
element,
'gr-access-section'
);
await queryAll<GrPermission>(
grAccessSection,
'gr-permission'
)[2].handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Modify a section reference.
element.local!['refs/*'].updatedId = 'refs/for/bar';
element.local!['refs/*'].modified = true;
await element.updateComplete;
expectedInput = {
add: {
'refs/for/bar': {
modified: true,
updatedId: 'refs/for/bar',
permissions: {
owner: {
rules: {
234: {action: 'ALLOW'},
123: {action: 'DENY'},
},
},
read: {
rules: {
234: {action: 'ALLOW'},
},
},
'label-Code-Review': {
added: true,
rules: {
Maintainers: {
min: -2,
max: 2,
action: 'ALLOW',
added: true,
},
},
label: 'Code-Review',
},
},
},
},
remove: {
'refs/*': {
permissions: {},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Delete a section.
element.local!['refs/*'].deleted = true;
await element.updateComplete;
expectedInput = {
add: {},
remove: {
'refs/*': {
permissions: {},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
});
test('add and remove and re-add ref', async () => {
// refs/for/* is added
queryAndAssert<GrButton>(element, '#addReferenceBtn').click();
await element.updateComplete;
// refs/for/* is removed
element.handleAddedSectionRemoved(1);
await element.updateComplete;
// refs/for/* is re-added without extra starts
queryAndAssert<GrButton>(element, '#addReferenceBtn').click();
await element.updateComplete;
assert.equal(element.sections![1].id, 'refs/for/*');
});
test('computeAddAndRemove new section', async () => {
// Add a new permission to a section
let expectedInput = {};
expectedInput = {
add: {
'refs/for/*': {
added: true,
permissions: {},
},
},
remove: {},
};
queryAndAssert<GrButton>(element, '#addReferenceBtn').click();
await element.updateComplete;
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
expectedInput = {
add: {
'refs/for/*': {
added: true,
permissions: {
'label-Code-Review': {
added: true,
rules: {},
label: 'Code-Review',
},
},
},
},
remove: {},
};
const newSection = queryAll<GrAccessSection>(
element,
'gr-access-section'
)[1];
newSection.handleAddPermission();
await element.updateComplete;
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Add rule to the new permission.
expectedInput = {
add: {
'refs/for/*': {
added: true,
permissions: {
'label-Code-Review': {
added: true,
rules: {
Maintainers: {
action: 'ALLOW',
added: true,
max: 2,
min: -2,
},
},
label: 'Code-Review',
},
},
},
},
remove: {},
};
await queryAndAssert<GrPermission>(
newSection,
'gr-permission'
).handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Modify a the reference from the default value.
element.local!['refs/for/*'].updatedId = 'refs/for/new';
await element.updateComplete;
expectedInput = {
add: {
'refs/for/new': {
added: true,
updatedId: 'refs/for/new',
permissions: {
'label-Code-Review': {
added: true,
rules: {
Maintainers: {
action: 'ALLOW',
added: true,
max: 2,
min: -2,
},
},
label: 'Code-Review',
},
},
},
},
remove: {},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
});
test('computeAddAndRemove combinations', async () => {
// Modify rule and delete permission that it is inside of.
element.local!['refs/*'].permissions.owner.rules[123].modified = true;
element.local!['refs/*'].permissions.owner.deleted = true;
await element.updateComplete;
let expectedInput = {};
expectedInput = {
add: {},
remove: {
'refs/*': {
permissions: {
owner: {rules: {}},
},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Delete rule and delete permission that it is inside of.
element.local!['refs/*'].permissions.owner.rules[123].modified = false;
element.local!['refs/*'].permissions.owner.rules[123].deleted = true;
await element.updateComplete;
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Also modify a different rule inside of another permission.
element.local!['refs/*'].permissions.read.modified = true;
await element.updateComplete;
expectedInput = {
add: {
'refs/*': {
permissions: {
read: {
modified: true,
rules: {
234: {action: 'ALLOW'},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {
owner: {rules: {}},
read: {rules: {}},
},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Modify both permissions with an exclusive bit. Owner is still
// deleted.
element.local!['refs/*'].permissions.owner.exclusive = true;
element.local!['refs/*'].permissions.owner.modified = true;
element.local!['refs/*'].permissions.read.exclusive = true;
element.local!['refs/*'].permissions.read.modified = true;
await element.updateComplete;
expectedInput = {
add: {
'refs/*': {
permissions: {
read: {
exclusive: true,
modified: true,
rules: {
234: {action: 'ALLOW'},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {
owner: {rules: {}},
read: {rules: {}},
},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Add a rule to the existing permission;
const grAccessSection = queryAndAssert<GrAccessSection>(
element,
'gr-access-section'
);
await queryAll<GrPermission>(
grAccessSection,
'gr-permission'
)[1].handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
expectedInput = {
add: {
'refs/*': {
permissions: {
read: {
exclusive: true,
modified: true,
rules: {
234: {action: 'ALLOW'},
Maintainers: {action: 'ALLOW', added: true},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {
owner: {rules: {}},
read: {rules: {}},
},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Change one of the refs
element.local!['refs/*'].updatedId = 'refs/for/bar';
element.local!['refs/*'].modified = true;
await element.updateComplete;
expectedInput = {
add: {
'refs/for/bar': {
modified: true,
updatedId: 'refs/for/bar',
permissions: {
read: {
exclusive: true,
modified: true,
rules: {
234: {action: 'ALLOW'},
Maintainers: {action: 'ALLOW', added: true},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
expectedInput = {
add: {},
remove: {
'refs/*': {
permissions: {},
},
},
};
element.local!['refs/*'].deleted = true;
await element.updateComplete;
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Add a new section.
queryAndAssert<GrButton>(element, '#addReferenceBtn').click();
await element.updateComplete;
let newSection = queryAll<GrAccessSection>(
element,
'gr-access-section'
)[1];
newSection.handleAddPermission();
await element.updateComplete;
await queryAndAssert<GrPermission>(
newSection,
'gr-permission'
).handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
// Modify a the reference from the default value.
element.local!['refs/for/*'].updatedId = 'refs/for/new';
await element.updateComplete;
expectedInput = {
add: {
'refs/for/new': {
added: true,
updatedId: 'refs/for/new',
permissions: {
'label-Code-Review': {
added: true,
rules: {
Maintainers: {
action: 'ALLOW',
added: true,
max: 2,
min: -2,
},
},
label: 'Code-Review',
},
},
},
},
remove: {
'refs/*': {
permissions: {},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Modify newly added rule inside new ref.
element.local!['refs/for/*'].permissions['label-Code-Review'].rules[
'Maintainers'
].modified = true;
await element.updateComplete;
expectedInput = {
add: {
'refs/for/new': {
added: true,
updatedId: 'refs/for/new',
permissions: {
'label-Code-Review': {
added: true,
rules: {
Maintainers: {
action: 'ALLOW',
added: true,
modified: true,
max: 2,
min: -2,
},
},
label: 'Code-Review',
},
},
},
},
remove: {
'refs/*': {
permissions: {},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
// Add a second new section.
queryAndAssert<GrButton>(element, '#addReferenceBtn').click();
await element.updateComplete;
newSection = queryAll<GrAccessSection>(element, 'gr-access-section')[2];
newSection.handleAddPermission();
await element.updateComplete;
await queryAndAssert<GrPermission>(
newSection,
'gr-permission'
).handleAddRuleItem({
detail: {value: 'Maintainers'},
} as AutocompleteCommitEvent);
// Modify a the reference from the default value.
element.local!['refs/for/**'].updatedId = 'refs/for/new2';
await element.updateComplete;
expectedInput = {
add: {
'refs/for/new': {
added: true,
updatedId: 'refs/for/new',
permissions: {
'label-Code-Review': {
added: true,
rules: {
Maintainers: {
action: 'ALLOW',
added: true,
modified: true,
max: 2,
min: -2,
},
},
label: 'Code-Review',
},
},
},
'refs/for/new2': {
added: true,
updatedId: 'refs/for/new2',
permissions: {
'label-Code-Review': {
added: true,
rules: {
Maintainers: {
action: 'ALLOW',
added: true,
max: 2,
min: -2,
},
},
label: 'Code-Review',
},
},
},
},
remove: {
'refs/*': {
permissions: {},
},
},
};
assert.deepEqual(element.computeAddAndRemove(), expectedInput);
});
test('Unsaved added refs are discarded when edit cancelled', async () => {
// Unsaved changes are discarded when editing is cancelled.
queryAndAssert<GrButton>(element, '#editBtn').click();
await element.updateComplete;
assert.equal(element.sections!.length, 1);
assert.equal(Object.keys(element.local!).length, 1);
queryAndAssert<GrButton>(element, '#addReferenceBtn').click();
await element.updateComplete;
assert.equal(element.sections!.length, 2);
assert.equal(Object.keys(element.local!).length, 2);
queryAndAssert<GrButton>(element, '#editBtn').click();
await element.updateComplete;
assert.equal(element.sections!.length, 1);
assert.equal(Object.keys(element.local!).length, 1);
});
test('handleSave', async () => {
const repoAccessInput = {
add: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {action: 'DENY', modified: true},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {},
},
},
},
},
},
};
stubRestApi('getRepoAccessRights').returns(
Promise.resolve(JSON.parse(JSON.stringify(accessRes)))
);
const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
let resolver: (value: Response | PromiseLike<Response>) => void;
const saveStub = stubRestApi('setRepoAccessRights').returns(
new Promise(r => (resolver = r))
);
element.repo = 'test-repo' as RepoName;
await element.updateComplete;
sinon.stub(element, 'computeAddAndRemove').returns(repoAccessInput);
assert.equal(
queryAndAssert<GrButton>(element, '#saveBtn').hasAttribute('disabled'),
true
);
element.modified = true;
await element.updateComplete;
assert.equal(
queryAndAssert<GrButton>(element, '#saveBtn').hasAttribute('disabled'),
false
);
queryAndAssert<GrButton>(element, '#saveBtn').click();
await element.updateComplete;
assert.equal(
queryAndAssert<GrButton>(element, '#saveBtn').hasAttribute('loading'),
true
);
resolver!({status: 200} as Response);
await element.updateComplete;
assert.isTrue(saveStub.called);
assert.isTrue(setUrlStub.notCalled);
});
test('saveBtn remains disabled when require_change_for_config_update is set', async () => {
const repoAccessInput = {
add: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {action: 'DENY', modified: true},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {},
},
},
},
},
},
};
stubRestApi('getRepoAccessRights').returns(
Promise.resolve(
JSON.parse(
JSON.stringify({
...accessRes,
require_change_for_config_update: true,
})
)
)
);
element.repo = 'test-repo' as RepoName;
await element.updateComplete;
sinon.stub(element, 'computeAddAndRemove').returns(repoAccessInput);
assert.equal(
queryAndAssert<GrButton>(element, '#saveBtn').hasAttribute('disabled'),
true
);
element.modified = true;
await element.updateComplete;
assert.equal(element.disableSaveWithoutReview, true);
assert.equal(
queryAndAssert<GrButton>(element, '#saveBtn').hasAttribute('disabled'),
true
);
});
test('handleSaveForReview', async () => {
const repoAccessInput = {
add: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {action: 'DENY', modified: true},
},
},
},
},
},
remove: {
'refs/*': {
permissions: {
owner: {
rules: {
123: {},
},
},
},
},
},
};
stubRestApi('getRepoAccessRights').returns(
Promise.resolve(JSON.parse(JSON.stringify(accessRes)))
);
const setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
let resolver: (value: ChangeInfo | PromiseLike<ChangeInfo>) => void;
const saveForReviewStub = stubRestApi(
'setRepoAccessRightsForReview'
).returns(new Promise(r => (resolver = r)));
element.repo = 'test-repo' as RepoName;
sinon.stub(element, 'computeAddAndRemove').returns(repoAccessInput);
element.modified = true;
queryAndAssert<GrButton>(element, '#saveReviewBtn').click();
await element.updateComplete;
assert.equal(
queryAndAssert<GrButton>(element, '#saveReviewBtn').hasAttribute(
'loading'
),
true
);
resolver!(createChange());
await element.updateComplete;
assert.isTrue(saveForReviewStub.called);
assert.isTrue(setUrlStub.called);
assert.isTrue(
setUrlStub.lastCall.args?.[0]?.includes(`${createChange()._number}`)
);
});
});
});