| /** | |
| * @license | |
| * Copyright (C) 2024 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| import {assert} from '@open-wc/testing'; | |
| import { | |
| computeApprovalAndInfo, | |
| getChangeAccounts, | |
| getFileOwnership, | |
| shouldHide, | |
| } from './gr-files'; | |
| import { | |
| FileOwnership, | |
| FileStatus, | |
| OwnerOrGroupOwner, | |
| PatchRange, | |
| UserRole, | |
| } from './owners-model'; | |
| import { | |
| AccountInfo, | |
| ApprovalInfo, | |
| ChangeInfo, | |
| ChangeStatus, | |
| DetailedLabelInfo, | |
| SubmitRequirementResultInfo, | |
| ReviewerState, | |
| } from '@gerritcodereview/typescript-api/rest-api'; | |
| import {FilesOwners, OwnersLabels} from './owners-service'; | |
| import {deepEqual} from './utils'; | |
| import {getRandom} from './test-utils'; | |
| suite('owners status tests', () => { | |
| const path = 'readme.md'; | |
| const approvedPath = 'db.sql'; | |
| const autoApprovedPath = 'foo.java'; | |
| const filesOwners = { | |
| files: {[path]: [{name: 'John', id: 1}]}, | |
| files_approved: {[approvedPath]: [{name: 'Merry', id: 2}]}, | |
| files_auto_approved: {[autoApprovedPath]: [{name: 'Merry', id: 2}]}, | |
| } as unknown as FilesOwners; | |
| suite('shouldHide tests', () => { | |
| const loggedIn = getRandom(UserRole.CHANGE_OWNER, UserRole.OTHER); | |
| test('shouldHide - should be `true` when change is not defined', () => { | |
| const undefinedChange = undefined; | |
| const definedPatchRange = {} as unknown as PatchRange; | |
| assert.equal( | |
| shouldHide(undefinedChange, definedPatchRange, filesOwners, loggedIn), | |
| true | |
| ); | |
| }); | |
| test('shouldHide - should be `true` when patch range is not defined', () => { | |
| const definedChange = {} as unknown as ChangeInfo; | |
| const undefinedPatchRange = undefined; | |
| assert.equal( | |
| shouldHide(definedChange, undefinedPatchRange, filesOwners, loggedIn), | |
| true | |
| ); | |
| }); | |
| test('shouldHide - should be `true` when change is abandoned', () => { | |
| const abandonedChange = { | |
| status: ChangeStatus.ABANDONED, | |
| } as unknown as ChangeInfo; | |
| const definedPatchRange = {} as unknown as PatchRange; | |
| assert.equal( | |
| shouldHide(abandonedChange, definedPatchRange, filesOwners, loggedIn), | |
| true | |
| ); | |
| }); | |
| test('shouldHide - should be `true` when change is merged', () => { | |
| const mergedChange = { | |
| status: ChangeStatus.MERGED, | |
| } as unknown as ChangeInfo; | |
| const definedPatchRange = {} as unknown as PatchRange; | |
| assert.equal( | |
| shouldHide(mergedChange, definedPatchRange, filesOwners, loggedIn), | |
| true | |
| ); | |
| }); | |
| test('shouldHide - should be `true` if not on the latest PS', () => { | |
| const changeWithPs2 = { | |
| status: ChangeStatus.NEW, | |
| revisions: { | |
| current_rev: {_number: 2}, | |
| }, | |
| current_revision: 'current_rev', | |
| } as unknown as ChangeInfo; | |
| const patchRangeOnPs1 = {patchNum: 1} as unknown as PatchRange; | |
| assert.equal( | |
| shouldHide(changeWithPs2, patchRangeOnPs1, filesOwners, loggedIn), | |
| true | |
| ); | |
| }); | |
| const change = { | |
| status: ChangeStatus.NEW, | |
| revisions: { | |
| current_rev: {_number: 1}, | |
| }, | |
| current_revision: 'current_rev', | |
| } as unknown as ChangeInfo; | |
| const patchRange = {patchNum: 1} as unknown as PatchRange; | |
| test('shouldHide - should be `true` when change has no submit requirements', () => { | |
| assert.equal(shouldHide(change, patchRange, filesOwners, loggedIn), true); | |
| }); | |
| test('shouldHide - should be `true` when change has no `Owner-Approval` submit requirements', () => { | |
| const changeWithDifferentSubmitReqs = { | |
| ...change, | |
| submit_requirements: [ | |
| {submittability_expression_result: {expression: 'other'}}, | |
| ] as unknown as SubmitRequirementResultInfo[], | |
| }; | |
| assert.equal( | |
| shouldHide( | |
| changeWithDifferentSubmitReqs, | |
| patchRange, | |
| filesOwners, | |
| loggedIn | |
| ), | |
| true | |
| ); | |
| }); | |
| const submitRequirements = getRandom({ | |
| submit_requirements: [ | |
| {submittability_expression_result: {expression: 'has:approval_owners'}}, | |
| { | |
| submittability_expression_result: { | |
| expression: 'rule:owners~OwnersSubmitRequirement', | |
| }, | |
| }, | |
| ] as unknown as SubmitRequirementResultInfo[], | |
| }); | |
| const changeWithSubmitRequirementOrRecord = { | |
| ...change, | |
| ...submitRequirements, | |
| }; | |
| test('shouldHide - should be `true` when user is anonymous', () => { | |
| const anonymous = UserRole.ANONYMOUS; | |
| assert.equal( | |
| shouldHide( | |
| changeWithSubmitRequirementOrRecord, | |
| patchRange, | |
| filesOwners, | |
| anonymous | |
| ), | |
| true | |
| ); | |
| }); | |
| test('shouldHide - should be `true` when files owners are `undefined`', () => { | |
| const undefinedFilesOwners = getRandom( | |
| undefined, | |
| {} | |
| ) as unknown as FilesOwners; | |
| assert.equal( | |
| shouldHide( | |
| changeWithSubmitRequirementOrRecord, | |
| patchRange, | |
| undefinedFilesOwners, | |
| loggedIn | |
| ), | |
| true | |
| ); | |
| }); | |
| test('shouldHide - should be `false` when change has all files approved', () => { | |
| const allFilesApproved = { | |
| approved_files: {[approvedPath]: [{name: 'Merry', id: 2}]}, | |
| } as unknown as FilesOwners; | |
| assert.equal( | |
| shouldHide( | |
| changeWithSubmitRequirementOrRecord, | |
| patchRange, | |
| allFilesApproved, | |
| loggedIn | |
| ), | |
| true | |
| ); | |
| }); | |
| test('shouldHide - should be `false` when change has no files approved', () => { | |
| const noApprovedFiles = { | |
| files: {[path]: [{name: 'John', id: 1}]}, | |
| } as unknown as FilesOwners; | |
| assert.equal( | |
| shouldHide( | |
| changeWithSubmitRequirementOrRecord, | |
| patchRange, | |
| noApprovedFiles, | |
| loggedIn | |
| ), | |
| false | |
| ); | |
| }); | |
| test('shouldHide - should be `false` when change has both approved and not approved files', () => { | |
| assert.equal( | |
| shouldHide( | |
| changeWithSubmitRequirementOrRecord, | |
| patchRange, | |
| filesOwners, | |
| loggedIn | |
| ), | |
| false | |
| ); | |
| }); | |
| test('shouldHide - should be `false` when in edit mode', () => { | |
| const patchRangeWithoutPatchNum = {} as unknown as PatchRange; | |
| assert.equal( | |
| shouldHide(change, patchRangeWithoutPatchNum, filesOwners, loggedIn), | |
| false | |
| ); | |
| }); | |
| }); | |
| suite('getFileOwnership tests', () => { | |
| const emptyFilesOwners = {} as unknown as FilesOwners; | |
| test('getFileOwnership - should be `undefined` when path is `undefined', () => { | |
| const undefinedPath = undefined; | |
| assert.equal( | |
| getFileOwnership(undefinedPath, emptyFilesOwners), | |
| undefined | |
| ); | |
| }); | |
| test('getFileOwnership - should be `undefined` when file owners are `undefined', () => { | |
| const undefinedFileOwners = undefined; | |
| assert.equal(getFileOwnership(path, undefinedFileOwners), undefined); | |
| }); | |
| test('getFileOwnership - should return `FileOwnership` with `APPROVED` fileStatus when file is approved', () => { | |
| assert.equal( | |
| deepEqual(getFileOwnership(approvedPath, filesOwners), { | |
| fileStatus: FileStatus.APPROVED, | |
| owners: [{name: 'Merry', id: 2}], | |
| } as FileOwnership), | |
| true | |
| ); | |
| }); | |
| test('getFileOwnership - should return owners from `files_auto_approved` when file is auto-approved', () => { | |
| const filesOwnersWithExplicitAndAutoApprovers = { | |
| files_approved: { | |
| [approvedPath]: [ | |
| {name: 'Merry', id: 2}, | |
| {name: 'John', id: 1}, | |
| ], | |
| }, | |
| files_auto_approved: { | |
| [autoApprovedPath]: [{name: 'Merry', id: 2}], | |
| }, | |
| } as unknown as FilesOwners; | |
| assert.equal( | |
| deepEqual( | |
| getFileOwnership( | |
| autoApprovedPath, | |
| filesOwnersWithExplicitAndAutoApprovers | |
| ), | |
| { | |
| fileStatus: FileStatus.AUTO_APPROVED, | |
| owners: [{name: 'Merry', id: 2}], | |
| } as FileOwnership | |
| ), | |
| true | |
| ); | |
| }); | |
| test('getFileOwnership - should return `FileOwnership` with `NOT_OWNED` fileStatus when file has no owner', () => { | |
| assert.equal( | |
| deepEqual(getFileOwnership(path, emptyFilesOwners), { | |
| fileStatus: FileStatus.NOT_OWNED, | |
| } as FileOwnership), | |
| true | |
| ); | |
| }); | |
| test('getFileOwnership - should return `FileOwnership` with `NEEDS_APPROVAL` fileStatus when file has owner', () => { | |
| const fileOwnersWithPathOwner = { | |
| files: {[path]: [{name: 'John', id: 1}]}, | |
| } as unknown as FilesOwners; | |
| assert.equal( | |
| deepEqual(getFileOwnership(path, fileOwnersWithPathOwner), { | |
| fileStatus: FileStatus.NEEDS_APPROVAL, | |
| owners: [{name: 'John', id: 1}], | |
| } as FileOwnership), | |
| true | |
| ); | |
| }); | |
| }); | |
| suite('computeApprovalAndInfo tests', () => { | |
| const account = 1; | |
| const fileOwner = { | |
| owner: {_account_id: account} as unknown as AccountInfo, | |
| } as unknown as OwnerOrGroupOwner; | |
| const label = 'Code-Review'; | |
| const crPlus1OwnersVote = { | |
| [`${account}`]: {[label]: 1}, | |
| } as unknown as OwnersLabels; | |
| const changeWithLabels = { | |
| labels: { | |
| [label]: { | |
| all: [ | |
| { | |
| value: 1, | |
| date: '2024-10-22 17:26:21.000000000', | |
| permitted_voting_range: { | |
| min: -2, | |
| max: 2, | |
| }, | |
| _account_id: account, | |
| }, | |
| ], | |
| values: { | |
| '-2': 'This shall not be submitted', | |
| '-1': 'I would prefer this is not submitted as is', | |
| ' 0': 'No score', | |
| '+1': 'Looks good to me, but someone else must approve', | |
| '+2': 'Looks good to me, approved', | |
| }, | |
| description: '', | |
| default_value: 0, | |
| }, | |
| }, | |
| } as unknown as ChangeInfo; | |
| test('computeApprovalAndInfo - should be `undefined` when change is `undefined', () => { | |
| const undefinedChange = undefined; | |
| assert.equal( | |
| computeApprovalAndInfo(fileOwner, crPlus1OwnersVote, undefinedChange), | |
| undefined | |
| ); | |
| }); | |
| test('computeApprovalAndInfo - should be `undefined` when there is no owners vote', () => { | |
| const emptyOwnersVote = {}; | |
| assert.equal( | |
| computeApprovalAndInfo(fileOwner, emptyOwnersVote, changeWithLabels), | |
| undefined | |
| ); | |
| }); | |
| test('computeApprovalAndInfo - should be `undefined` for default owners vote', () => { | |
| const defaultOwnersVote = {[label]: 0} as unknown as OwnersLabels; | |
| assert.equal( | |
| computeApprovalAndInfo(fileOwner, defaultOwnersVote, changeWithLabels), | |
| undefined | |
| ); | |
| }); | |
| test('computeApprovalAndInfo - should be computed for CR+1 owners vote', () => { | |
| const expectedApproval = { | |
| value: 1, | |
| date: '2024-10-22 17:26:21.000000000', | |
| permitted_voting_range: { | |
| min: -2, | |
| max: 2, | |
| }, | |
| _account_id: account, | |
| } as unknown as ApprovalInfo; | |
| const expectedInfo = { | |
| all: [expectedApproval], | |
| values: { | |
| '-2': 'This shall not be submitted', | |
| '-1': 'I would prefer this is not submitted as is', | |
| ' 0': 'No score', | |
| '+1': 'Looks good to me, but someone else must approve', | |
| '+2': 'Looks good to me, approved', | |
| }, | |
| description: '', | |
| default_value: 0, | |
| } as unknown as DetailedLabelInfo; | |
| assert.equal( | |
| deepEqual( | |
| computeApprovalAndInfo( | |
| fileOwner, | |
| crPlus1OwnersVote, | |
| changeWithLabels | |
| ), | |
| [expectedApproval, expectedInfo] | |
| ), | |
| true | |
| ); | |
| }); | |
| }); | |
| suite('getChangeAccounts tests', () => { | |
| test('getChangeAccounts - should return empty map when change is `undefined', () => { | |
| const undefinedChange = undefined; | |
| const accounts = getChangeAccounts(undefinedChange); | |
| assert.equal(accounts.byAccountId.size, 0); | |
| assert.equal(accounts.byEmailWithoutDomain.size, 0); | |
| assert.equal(accounts.byFullName.size, 0); | |
| }); | |
| test('getChangeAccounts - should return map with owner when change has only owner and empty reviewers defined', () => { | |
| const owner = account(1); | |
| const changeWithOwner = { | |
| owner, | |
| reviewers: {}, | |
| } as unknown as ChangeInfo; | |
| const accounts = getChangeAccounts(changeWithOwner); | |
| assert.equal( | |
| deepEqual(accounts.byAccountId, new Map([[1, owner]])), | |
| true | |
| ); | |
| assert.equal( | |
| deepEqual(accounts.byEmailWithoutDomain, new Map([['1_email', owner]])), | |
| true | |
| ); | |
| assert.equal( | |
| deepEqual(accounts.byFullName, new Map([['1_name', owner]])), | |
| true | |
| ); | |
| }); | |
| test('getChangeAccounts - should return map with owner, submitter and reviewers', () => { | |
| const owner = account(1); | |
| const submitter = account(2); | |
| const reviewer = account(3); | |
| const ccReviewer = account(4); | |
| const change = { | |
| owner, | |
| submitter, | |
| reviewers: { | |
| [ReviewerState.REVIEWER]: [reviewer], | |
| [ReviewerState.CC]: [ccReviewer], | |
| }, | |
| } as unknown as ChangeInfo; | |
| const accounts = getChangeAccounts(change); | |
| assert.equal( | |
| deepEqual( | |
| accounts.byAccountId, | |
| new Map([ | |
| [1, owner], | |
| [2, submitter], | |
| [3, reviewer], | |
| [4, ccReviewer], | |
| ]) | |
| ), | |
| true | |
| ); | |
| assert.equal( | |
| deepEqual( | |
| accounts.byEmailWithoutDomain, | |
| new Map([ | |
| ['1_email', owner], | |
| ['2_email', submitter], | |
| ['3_email', reviewer], | |
| ['4_email', ccReviewer], | |
| ]) | |
| ), | |
| true | |
| ); | |
| assert.equal( | |
| deepEqual( | |
| accounts.byFullName, | |
| new Map([ | |
| ['1_name', owner], | |
| ['2_name', submitter], | |
| ['3_name', reviewer], | |
| ['4_name', ccReviewer], | |
| ]) | |
| ), | |
| true | |
| ); | |
| }); | |
| }); | |
| }); | |
| function account(id: number) { | |
| return { | |
| _account_id: id, | |
| email: `${id}_email@example.com`, | |
| name: `${id}_name`, | |
| } as unknown as AccountInfo; | |
| } |