From e72008d7f103b7d78ffffdf14dc6e502df0dccab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Apr 2020 14:31:33 -0600 Subject: [PATCH 1/4] Fix end-to-end tests for end-to-end encryption verification Fixes https://github.com/vector-im/riot-web/issues/13226 This isn't the fastest route, but it is a predictable route for the happy path we probably want to test. For example, Alice will already be staring at the DM and could easily accept the verification there, but we probably want to make sure that the toast is present and does the right thing. Similarly, neither of them need to verify that there's green shields everywhere, they should be implied, however an explicit check follows a real user's gaze. --- src/components/views/right_panel/UserInfo.js | 3 +- .../views/right_panel/VerificationPanel.js | 9 +- .../src/scenarios/directory.js | 2 +- .../src/scenarios/e2e-encryption.js | 67 ++++++------- .../src/scenarios/lazy-loading.js | 2 +- test/end-to-end-tests/src/session.js | 4 + .../src/usecases/create-room.js | 39 +++++++- .../src/usecases/room-settings.js | 92 +++++++++++++++++- test/end-to-end-tests/src/usecases/verify.js | 97 ++++++++++++++----- 9 files changed, 244 insertions(+), 71 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 862e4f7897..979bac23e6 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1308,8 +1308,9 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => { useHasCrossSigningKeys(cli, member, canVerify, setUpdating ); if (canVerify) { + // Note: mx_UserInfo_verifyButton is for the end-to-end tests verifyButton = ( - { + { if (hasCrossSigningKeys) { verifyUser(member); } else { diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js index b60cc234eb..67efd29d27 100644 --- a/src/components/views/right_panel/VerificationPanel.js +++ b/src/components/views/right_panel/VerificationPanel.js @@ -123,10 +123,17 @@ export default class VerificationPanel extends React.PureComponent { const sasLabel = showQR ? _t("If you can't scan the code above, verify by comparing unique emoji.") : _t("Verify by comparing unique emoji."); + + // Note: mx_VerificationPanel_verifyByEmojiButton is for the end-to-end tests sasBlock =

{_t("Verify by emoji")}

{sasLabel}

- + {_t("Verify by emoji")}
; diff --git a/test/end-to-end-tests/src/scenarios/directory.js b/test/end-to-end-tests/src/scenarios/directory.js index ca2f99f192..b5be9ed4f4 100644 --- a/test/end-to-end-tests/src/scenarios/directory.js +++ b/test/end-to-end-tests/src/scenarios/directory.js @@ -20,7 +20,7 @@ const join = require('../usecases/join'); const sendMessage = require('../usecases/send-message'); const {receiveMessage} = require('../usecases/timeline'); const {createRoom} = require('../usecases/create-room'); -const changeRoomSettings = require('../usecases/room-settings'); +const {changeRoomSettings} = require('../usecases/room-settings'); module.exports = async function roomDirectoryScenarios(alice, bob) { console.log(" creating a public room and join through directory:"); diff --git a/test/end-to-end-tests/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js index f30b814644..586b3a0404 100644 --- a/test/end-to-end-tests/src/scenarios/e2e-encryption.js +++ b/test/end-to-end-tests/src/scenarios/e2e-encryption.js @@ -1,6 +1,6 @@ /* Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,42 +15,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -// TODO: Update test for cross signing -// https://github.com/vector-im/riot-web/issues/13226 +const sendMessage = require('../usecases/send-message'); +const acceptInvite = require('../usecases/accept-invite'); +const invite = require('../usecases/invite'); +const {receiveMessage} = require('../usecases/timeline'); +const {createDm} = require('../usecases/create-room'); +const {checkRoomSettings} = require('../usecases/room-settings'); +const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify'); +const assert = require('assert'); -module.exports = async function() { - console.log(" this is supposed to be an e2e test, but it's broken"); +module.exports = async function e2eEncryptionScenarios(alice, bob) { + console.log(" creating an e2e encrypted DM and join through invite:"); + await createDm(bob, ['@alice:localhost']); + await checkRoomSettings(bob, {encryption: true}); // for sanity, should be e2e-by-default + await acceptInvite(alice, 'bob'); + // do sas verifcation + bob.log.step(`starts SAS verification with ${alice.username}`); + const bobSasPromise = startSasVerifcation(bob, alice.username); + const aliceSasPromise = acceptSasVerification(alice, bob.username); + // wait in parallel, so they don't deadlock on each other + // the logs get a bit messy here, but that's fine enough for debugging (hopefully) + const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]); + assert.deepEqual(bobSas, aliceSas); + bob.log.done(`done (match for ${bobSas.join(", ")})`); + const aliceMessage = "Guess what I just heard?!"; + await sendMessage(alice, aliceMessage); + await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); + const bobMessage = "You've got to tell me!"; + await sendMessage(bob, bobMessage); + await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); }; - -// const sendMessage = require('../usecases/send-message'); -// const acceptInvite = require('../usecases/accept-invite'); -// const invite = require('../usecases/invite'); -// const {receiveMessage} = require('../usecases/timeline'); -// const {createRoom} = require('../usecases/create-room'); -// const changeRoomSettings = require('../usecases/room-settings'); -// const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify'); -// const assert = require('assert'); -// -// module.exports = async function e2eEncryptionScenarios(alice, bob) { -// console.log(" creating an e2e encrypted room and join through invite:"); -// const room = "secrets"; -// await createRoom(bob, room); -// await changeRoomSettings(bob, {encryption: true}); -// // await cancelKeyBackup(bob); -// await invite(bob, "@alice:localhost"); -// await acceptInvite(alice, room); -// // do sas verifcation -// bob.log.step(`starts SAS verification with ${alice.username}`); -// const bobSasPromise = startSasVerifcation(bob, alice.username); -// const aliceSasPromise = acceptSasVerification(alice, bob.username); -// // wait in parallel, so they don't deadlock on each other -// const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]); -// assert.deepEqual(bobSas, aliceSas); -// bob.log.done(`done (match for ${bobSas.join(", ")})`); -// const aliceMessage = "Guess what I just heard?!"; -// await sendMessage(alice, aliceMessage); -// await receiveMessage(bob, {sender: "alice", body: aliceMessage, encrypted: true}); -// const bobMessage = "You've got to tell me!"; -// await sendMessage(bob, bobMessage); -// await receiveMessage(alice, {sender: "bob", body: bobMessage, encrypted: true}); -// }; diff --git a/test/end-to-end-tests/src/scenarios/lazy-loading.js b/test/end-to-end-tests/src/scenarios/lazy-loading.js index 0c45b0d083..6d321dc737 100644 --- a/test/end-to-end-tests/src/scenarios/lazy-loading.js +++ b/test/end-to-end-tests/src/scenarios/lazy-loading.js @@ -25,7 +25,7 @@ const { } = require('../usecases/timeline'); const {createRoom} = require('../usecases/create-room'); const {getMembersInMemberlist} = require('../usecases/memberlist'); -const changeRoomSettings = require('../usecases/room-settings'); +const {changeRoomSettings} = require('../usecases/room-settings'); const assert = require('assert'); module.exports = async function lazyLoadingScenarios(alice, bob, charlies) { diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js index f25c5056ad..55c2ed440c 100644 --- a/test/end-to-end-tests/src/session.js +++ b/test/end-to-end-tests/src/session.js @@ -75,6 +75,10 @@ module.exports = class RiotSession { return this.getElementProperty(field, 'outerHTML'); } + isChecked(field) { + return this.getElementProperty(field, 'checked'); + } + consoleLogs() { return this.consoleLog.buffer; } diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js index ab2d9b69b9..c40cbba096 100644 --- a/test/end-to-end-tests/src/usecases/create-room.js +++ b/test/end-to-end-tests/src/usecases/create-room.js @@ -27,7 +27,7 @@ async function createRoom(session, roomName, encrypted=false) { const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); const roomsIndex = roomListHeaderLabels.findIndex(l => l.toLowerCase().includes("rooms")); if (roomsIndex === -1) { - throw new Error("could not find room list section that contains rooms in header"); + throw new Error("could not find room list section that contains 'rooms' in header"); } const roomsHeader = roomListHeaders[roomsIndex]; const addRoomButton = await roomsHeader.$(".mx_RoomSubList_addRoom"); @@ -48,4 +48,39 @@ async function createRoom(session, roomName, encrypted=false) { session.log.done(); } -module.exports = {openRoomDirectory, createRoom}; +async function createDm(session, invitees) { + session.log.step(`creates DM with ${JSON.stringify(invitees)}`); + + const roomListHeaders = await session.queryAll('.mx_RoomSubList_labelContainer'); + const roomListHeaderLabels = await Promise.all(roomListHeaders.map(h => session.innerText(h))); + const dmsIndex = roomListHeaderLabels.findIndex(l => l.toLowerCase().includes('direct messages')); + if (dmsIndex === -1) { + throw new Error("could not find room list section that contains 'direct messages' in header"); + } + const dmsHeader = roomListHeaders[dmsIndex]; + const addRoomButton = await dmsHeader.$(".mx_RoomSubList_addRoom"); + await addRoomButton.click(); + + const inviteesEditor = await session.query('.mx_InviteDialog_editor textarea'); + for (const target of invitees) { + await session.replaceInputText(inviteesEditor, target); + await session.delay(1000); // give it a moment to figure out a suggestion + // find the suggestion and accept it + const suggestions = await session.queryAll('.mx_InviteDialog_roomTile_userId'); + const suggestionTexts = await Promise.all(suggestions.map(s => session.innerText(s))); + const suggestionIndex = suggestionTexts.indexOf(target); + if (suggestionIndex === -1) { + throw new Error(`failed to find a suggestion in the DM dialog to invite ${target} with`); + } + await suggestions[suggestionIndex].click(); + } + + // press the go button and hope for the best + const goButton = await session.query('.mx_InviteDialog_goButton'); + await goButton.click(); + + await session.query('.mx_MessageComposer'); + session.log.done(); +} + +module.exports = {openRoomDirectory, createRoom, createDm}; diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js index ab6d66ea6d..b705463965 100644 --- a/test/end-to-end-tests/src/usecases/room-settings.js +++ b/test/end-to-end-tests/src/usecases/room-settings.js @@ -30,18 +30,102 @@ async function setSettingsToggle(session, toggle, enabled) { } } -module.exports = async function changeRoomSettings(session, settings) { - session.log.startGroup(`changes the room settings`); +async function checkSettingsToggle(session, toggle, shouldBeEnabled) { + const className = await session.getElementProperty(toggle, "className"); + const checked = className.includes("mx_ToggleSwitch_on"); + if (checked === shouldBeEnabled) { + session.log.done('set as expected'); + } else { + // other logs in the area should give more context as to what this means. + throw new Error("settings toggle value didn't match expectation"); + } +} + +async function findTabs(session) { /// XXX delay is needed here, possibly because the header is being rerendered /// click doesn't do anything otherwise await session.delay(1000); const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[title=Settings]"); await settingsButton.click(); + //find tabs const tabButtons = await session.queryAll(".mx_RoomSettingsDialog .mx_TabbedView_tabLabel"); const tabLabels = await Promise.all(tabButtons.map(t => session.innerText(t))); const securityTabButton = tabButtons[tabLabels.findIndex(l => l.toLowerCase().includes("security"))]; + return {securityTabButton}; +} + +async function checkRoomSettings(session, expectedSettings) { + session.log.startGroup(`checks the room settings`); + + const {securityTabButton} = await findTabs(session); + const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); + const isDirectory = generalSwitches[0]; + + if (typeof expectedSettings.directory === 'boolean') { + session.log.step(`checks directory listing is ${expectedSettings.directory}`); + await checkSettingsToggle(session, isDirectory, expectedSettings.directory); + } + + if (expectedSettings.alias) { + session.log.step(`checks for local alias of ${expectedSettings.alias}`); + const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary"); + await summary.click(); + const localAliases = await session.query('.mx_RoomSettingsDialog .mx_AliasSettings .mx_EditableItem_item'); + const localAliasTexts = await Promise.all(localAliases.map(a => session.innerText(a))); + if (localAliasTexts.find(a => a.includes(expectedSettings.alias))) { + session.log.done("present"); + } else { + throw new Error(`could not find local alias ${expectedSettings.alias}`); + } + } + + securityTabButton.click(); + await session.delay(500); + const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); + const e2eEncryptionToggle = securitySwitches[0]; + + if (typeof expectedSettings.encryption === "boolean") { + session.log.step(`checks room e2e encryption is ${expectedSettings.encryption}`); + await checkSettingsToggle(session, e2eEncryptionToggle, expectedSettings.encryption); + } + + if (expectedSettings.visibility) { + session.log.step(`checks visibility is ${expectedSettings.visibility}`); + const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]"); + assert.equal(radios.length, 7); + const inviteOnly = radios[0]; + const publicNoGuests = radios[1]; + const publicWithGuests = radios[2]; + + let expectedRadio = null; + if (expectedSettings.visibility === "invite_only") { + expectedRadio = inviteOnly; + } else if (expectedSettings.visibility === "public_no_guests") { + expectedRadio = publicNoGuests; + } else if (expectedSettings.visibility === "public_with_guests") { + expectedRadio = publicWithGuests; + } else { + throw new Error(`unrecognized room visibility setting: ${expectedSettings.visibility}`); + } + if (await session.isChecked(expectedRadio)) { + session.log.done(); + } else { + throw new Error("room visibility is not as expected"); + } + } + + const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton"); + await closeButton.click(); + + session.log.endGroup(); +} + +async function changeRoomSettings(session, settings) { + session.log.startGroup(`changes the room settings`); + + const {securityTabButton} = await findTabs(session); const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch"); const isDirectory = generalSwitches[0]; @@ -100,4 +184,6 @@ module.exports = async function changeRoomSettings(session, settings) { await closeButton.click(); session.log.endGroup(); -}; +} + +module.exports = {checkRoomSettings, changeRoomSettings}; diff --git a/test/end-to-end-tests/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js index 5f507f96e6..7ff3f3d8bb 100644 --- a/test/end-to-end-tests/src/usecases/verify.js +++ b/test/end-to-end-tests/src/usecases/verify.js @@ -25,10 +25,19 @@ async function assertVerified(session) { } async function startVerification(session, name) { + session.log.step("opens their opponent's profile and starts verification"); await openMemberInfo(session, name); // click verify in member info - const firstVerifyButton = await session.query(".mx_MemberDeviceInfo_verify"); + const firstVerifyButton = await session.query(".mx_UserInfo_verifyButton"); await firstVerifyButton.click(); + + // wait for the animation to finish + await session.delay(1000); + + // click 'start verification' + const startVerifyButton = await session.query('.mx_UserInfo_container .mx_AccessibleButton_kind_primary'); + await startVerifyButton.click(); + session.log.done(); } async function getSasCodes(session) { @@ -38,33 +47,73 @@ async function getSasCodes(session) { return sasLabels; } -module.exports.startSasVerifcation = async function(session, name) { - await startVerification(session, name); - // expect "Verify device" dialog and click "Begin Verification" - await assertDialog(session, "Verify device"); - // click "Begin Verification" - await acceptDialog(session); +async function doSasVerification(session) { + session.log.step("hunts for the emoji to yell at their opponent"); const sasCodes = await getSasCodes(session); - // click "Verify" - await acceptDialog(session); - await assertVerified(session); - // click "Got it" when verification is done - await acceptDialog(session); + session.log.done(sasCodes); + + // Assume they match + session.log.step("assumes the emoji match"); + const matchButton = await session.query(".mx_VerificationShowSas .mx_AccessibleButton_kind_primary"); + await matchButton.click(); + session.log.done(); + + // Wait for a big green shield (universal sign that it worked) + session.log.step("waits for a green shield"); + await session.query(".mx_VerificationPanel_verified_section .mx_E2EIcon_verified"); + session.log.done(); + + // Click 'Got It' + session.log.step("confirms the green shield"); + const doneButton = await session.query(".mx_VerificationPanel_verified_section .mx_AccessibleButton_kind_primary"); + await doneButton.click(); + session.log.done(); + + // Wait a bit for the animation + session.log.step("confirms their opponent has a green shield"); + await session.delay(1000); + + // Verify that we now have a green shield in their name (proving it still works) + await session.query('.mx_UserInfo_profile .mx_E2EIcon_verified'); + session.log.done(); + + return sasCodes; +} + +module.exports.startSasVerifcation = async function(session, name) { + session.log.startGroup("starts verification"); + await startVerification(session, name); + + // expect to be waiting (the presence of a spinner is a good thing) + await session.query('.mx_UserInfo_container .mx_EncryptionInfo_spinner'); + + const sasCodes = await doSasVerification(session); + session.log.endGroup(); return sasCodes; }; module.exports.acceptSasVerification = async function(session, name) { - await assertDialog(session, "Incoming Verification Request"); - const opponentLabelElement = await session.query(".mx_IncomingSasDialog_opponentProfile h2"); - const opponentLabel = await session.innerText(opponentLabelElement); - assert(opponentLabel, name); - // click "Continue" button - await acceptDialog(session); - const sasCodes = await getSasCodes(session); - // click "Verify" - await acceptDialog(session); - await assertVerified(session); - // click "Got it" when verification is done - await acceptDialog(session); + session.log.startGroup("accepts verification"); + const requestToast = await session.query('.mx_Toast_icon_verification'); + + // verify the toast is for verification + const toastHeader = await requestToast.$("h2"); + const toastHeaderText = await session.innerText(toastHeader); + assert.equal(toastHeaderText, 'Verification Request'); + const toastDescription = await requestToast.$(".mx_Toast_description"); + const toastDescText = await session.innerText(toastDescription); + assert.equal(toastDescText.startsWith(name), true, + `verification opponent mismatch: expected to start with '${name}', got '${toastDescText}'`); + + // accept the verification + const acceptButton = await requestToast.$(".mx_AccessibleButton_kind_primary"); + await acceptButton.click(); + + // find the emoji button + const startEmojiButton = await session.query(".mx_VerificationPanel_verifyByEmojiButton"); + await startEmojiButton.click(); + + const sasCodes = await doSasVerification(session); + session.log.endGroup(); return sasCodes; }; From 5a9898591d0fb0edbebfefd06f9e5b0f34c19135 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Apr 2020 14:35:02 -0600 Subject: [PATCH 2/4] Appease the linter --- test/end-to-end-tests/src/scenarios/e2e-encryption.js | 1 - test/end-to-end-tests/src/usecases/verify.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/end-to-end-tests/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js index 586b3a0404..d31d2c0d57 100644 --- a/test/end-to-end-tests/src/scenarios/e2e-encryption.js +++ b/test/end-to-end-tests/src/scenarios/e2e-encryption.js @@ -17,7 +17,6 @@ limitations under the License. const sendMessage = require('../usecases/send-message'); const acceptInvite = require('../usecases/accept-invite'); -const invite = require('../usecases/invite'); const {receiveMessage} = require('../usecases/timeline'); const {createDm} = require('../usecases/create-room'); const {checkRoomSettings} = require('../usecases/room-settings'); diff --git a/test/end-to-end-tests/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js index 7ff3f3d8bb..719333c0d8 100644 --- a/test/end-to-end-tests/src/usecases/verify.js +++ b/test/end-to-end-tests/src/usecases/verify.js @@ -1,6 +1,6 @@ /* Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ limitations under the License. const assert = require('assert'); const {openMemberInfo} = require("./memberlist"); -const {assertDialog, acceptDialog} = require("./dialog"); async function assertVerified(session) { const dialogSubTitle = await session.innerText(await session.query(".mx_Dialog h2")); From 77df61065322d3991c087fc5021b44502870afdc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Apr 2020 14:38:05 -0600 Subject: [PATCH 3/4] Appease the linter a bit more --- test/end-to-end-tests/src/usecases/verify.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/end-to-end-tests/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js index 719333c0d8..98e73ad6b7 100644 --- a/test/end-to-end-tests/src/usecases/verify.js +++ b/test/end-to-end-tests/src/usecases/verify.js @@ -18,11 +18,6 @@ limitations under the License. const assert = require('assert'); const {openMemberInfo} = require("./memberlist"); -async function assertVerified(session) { - const dialogSubTitle = await session.innerText(await session.query(".mx_Dialog h2")); - assert(dialogSubTitle, "Verified!"); -} - async function startVerification(session, name) { session.log.step("opens their opponent's profile and starts verification"); await openMemberInfo(session, name); From 432dbab8cb978ececb3071c185208e24efc54960 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 17 Apr 2020 15:06:59 -0600 Subject: [PATCH 4/4] Fix start chat button variable name --- test/end-to-end-tests/src/usecases/create-room.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js index c40cbba096..7e219fd159 100644 --- a/test/end-to-end-tests/src/usecases/create-room.js +++ b/test/end-to-end-tests/src/usecases/create-room.js @@ -58,8 +58,8 @@ async function createDm(session, invitees) { throw new Error("could not find room list section that contains 'direct messages' in header"); } const dmsHeader = roomListHeaders[dmsIndex]; - const addRoomButton = await dmsHeader.$(".mx_RoomSubList_addRoom"); - await addRoomButton.click(); + const startChatButton = await dmsHeader.$(".mx_RoomSubList_addRoom"); + await startChatButton.click(); const inviteesEditor = await session.query('.mx_InviteDialog_editor textarea'); for (const target of invitees) {